Mercurial > hg > orthanc
changeset 2674:373b44af938f jobs
integration mainline->jobs
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 11 Jun 2018 16:30:13 +0200 |
parents | 8e0bc055d18c (diff) 62cc762d1fb0 (current diff) |
children | 3fc310ceb6d4 |
files | |
diffstat | 173 files changed, 15724 insertions(+), 4753 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Thu Jun 07 17:15:55 2018 +0200 +++ b/CMakeLists.txt Mon Jun 11 16:30:13 2018 +0200 @@ -55,6 +55,7 @@ set(ORTHANC_SERVER_SOURCES OrthancServer/DatabaseWrapper.cpp OrthancServer/DatabaseWrapperBase.cpp + OrthancServer/DicomInstanceOrigin.cpp OrthancServer/DicomInstanceToStore.cpp OrthancServer/ExportedResource.cpp OrthancServer/LuaScripting.cpp @@ -70,14 +71,6 @@ OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/QueryRetrieveHandler.cpp - OrthancServer/Scheduler/CallSystemCommand.cpp - OrthancServer/Scheduler/DeleteInstanceCommand.cpp - OrthancServer/Scheduler/ModifyInstanceCommand.cpp - OrthancServer/Scheduler/ServerCommandInstance.cpp - OrthancServer/Scheduler/ServerJob.cpp - OrthancServer/Scheduler/ServerScheduler.cpp - OrthancServer/Scheduler/StorePeerCommand.cpp - OrthancServer/Scheduler/StoreScuCommand.cpp OrthancServer/Search/HierarchicalMatcher.cpp OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/ListConstraint.cpp @@ -90,6 +83,17 @@ OrthancServer/ServerContext.cpp OrthancServer/ServerEnumerations.cpp OrthancServer/ServerIndex.cpp + OrthancServer/ServerJobs/ArchiveJob.cpp + OrthancServer/ServerJobs/DicomModalityStoreJob.cpp + OrthancServer/ServerJobs/LuaJobManager.cpp + OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp + OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp + OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp + OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp + OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp + OrthancServer/ServerJobs/OrthancJobUnserializer.cpp + OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp + OrthancServer/ServerJobs/ResourceModificationJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp )
--- a/Core/DicomFormat/DicomTag.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -39,9 +39,32 @@ #include <iostream> #include <iomanip> #include <stdio.h> +#include <string.h> namespace Orthanc -{ +{ + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } + + + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } + + bool DicomTag::operator< (const DicomTag& other) const { if (group_ < other.group_) @@ -74,6 +97,49 @@ } + bool DicomTag::ParseHexadecimal(DicomTag& tag, + const char* value) + { + size_t length = strlen(value); + + if (length == 9 && + isxdigit(value[0]) && + isxdigit(value[1]) && + isxdigit(value[2]) && + isxdigit(value[3]) && + (value[4] == '-' || value[4] == ',') && + isxdigit(value[5]) && + isxdigit(value[6]) && + isxdigit(value[7]) && + isxdigit(value[8])) + { + uint16_t group = GetTagValue(value); + uint16_t element = GetTagValue(value + 5); + tag = DicomTag(group, element); + return true; + } + else if (length == 8 && + isxdigit(value[0]) && + isxdigit(value[1]) && + isxdigit(value[2]) && + isxdigit(value[3]) && + isxdigit(value[4]) && + isxdigit(value[5]) && + isxdigit(value[6]) && + isxdigit(value[7])) + { + uint16_t group = GetTagValue(value); + uint16_t element = GetTagValue(value + 4); + tag = DicomTag(group, element); + return true; + } + else + { + return false; + } + } + + const char* DicomTag::GetMainTagsName() const { if (*this == DICOM_TAG_ACCESSION_NUMBER)
--- a/Core/DicomFormat/DicomTag.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomFormat/DicomTag.h Mon Jun 11 16:30:13 2018 +0200 @@ -88,6 +88,9 @@ std::string Format() const; + static bool ParseHexadecimal(DicomTag& tag, + const char* value); + friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag); static void AddTagsForModule(std::set<DicomTag>& target,
--- a/Core/DicomNetworking/DicomUserConnection.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -264,8 +264,6 @@ const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { - CheckIsOpen(); - DcmFileFormat dcmff; Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength)); @@ -284,23 +282,36 @@ bool isGeneric = IsGenericTransferSyntax(syntax); bool renegotiate; - if (isGeneric) + + if (!IsOpen()) + { + renegotiate = true; + } + else if (isGeneric) { // Are we making a generic-to-specific or specific-to-generic change of // the transfer syntax? If this is the case, renegotiate the connection. renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); + + if (renegotiate) + { + LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated"; + } } else { // We are using a specific transfer syntax. Renegotiate if the // current connection does not match this transfer syntax. renegotiate = (syntax != connection.GetPreferredTransferSyntax()); + + if (renegotiate) + { + LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; + } } if (renegotiate) { - LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; - if (isGeneric) { connection.ResetPreferredTransferSyntax(); @@ -313,7 +324,6 @@ if (!connection.IsOpen()) { - LOG(INFO) << "Renegotiating a C-Store association due to a change in the parameters"; connection.Open(); } @@ -785,13 +795,12 @@ } - DicomUserConnection::DicomUserConnection() : - pimpl_(new PImpl), - preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX), - localAet_("STORESCU"), - remoteAet_("ANY-SCP"), - remoteHost_("127.0.0.1") + void DicomUserConnection::DefaultSetup() { + preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX; + localAet_ = "STORESCU"; + remoteAet_ = "ANY-SCP"; + remoteHost_ = "127.0.0.1"; remotePort_ = 104; manufacturer_ = ModalityManufacturer_Generic; @@ -809,6 +818,24 @@ ResetStorageSOPClasses(); } + + + DicomUserConnection::DicomUserConnection() : + pimpl_(new PImpl) + { + DefaultSetup(); + } + + + DicomUserConnection::DicomUserConnection(const std::string& localAet, + const RemoteModalityParameters& remote) : + pimpl_(new PImpl) + { + DefaultSetup(); + SetLocalApplicationEntityTitle(localAet); + SetRemoteModality(remote); + } + DicomUserConnection::~DicomUserConnection() { @@ -1217,4 +1244,15 @@ << seconds << " seconds (0 = no timeout)"; defaultTimeout_ = seconds; } + + + bool DicomUserConnection::IsSameAssociation(const std::string& localAet, + const RemoteModalityParameters& remote) const + { + return (localAet_ == localAet && + remoteAet_ == remote.GetApplicationEntityTitle() && + remoteHost_ == remote.GetHost() && + remotePort_ == remote.GetPort() && + manufacturer_ == remote.GetManufacturer()); + } }
--- a/Core/DicomNetworking/DicomUserConnection.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.h Mon Jun 11 16:30:13 2018 +0200 @@ -77,11 +77,18 @@ void CheckStorageSOPClassesInvariant() const; + void DefaultSetup(); + public: DicomUserConnection(); ~DicomUserConnection(); + // This constructor corresponds to behavior of the old class + // "ReusableDicomUserConnection", without the call to "Open()" + DicomUserConnection(const std::string& localAet, + const RemoteModalityParameters& remote); + void SetRemoteModality(const RemoteModalityParameters& parameters); void SetLocalApplicationEntityTitle(const std::string& aet); @@ -201,5 +208,8 @@ ParsedDicomFile& query); static void SetDefaultTimeout(uint32_t seconds); + + bool IsSameAssociation(const std::string& localAet, + const RemoteModalityParameters& remote) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IDicomConnectionManager.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING) +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 + +namespace Orthanc +{ + // DICOM networking is disabled, this is just a void class + class IDicomConnectionManager : public boost::noncopyable + { + public: + virtual ~IDicomConnectionManager() + { + } + }; +} + +#else + +#include "DicomUserConnection.h" + +namespace Orthanc +{ + class IDicomConnectionManager : public boost::noncopyable + { + public: + virtual ~IDicomConnectionManager() + { + } + + class IResource : public boost::noncopyable + { + public: + virtual ~IResource() + { + } + + virtual DicomUserConnection& GetConnection() = 0; + }; + + virtual IResource* AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote) = 0; + }; +} + +#endif
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -36,6 +36,7 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include <boost/lexical_cast.hpp> #include <stdexcept> @@ -125,4 +126,26 @@ value.append(GetPort()); value.append(EnumerationToString(GetManufacturer())); } + + + + void RemoteModalityParameters::Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["AET"] = aet_; + target["Host"] = host_; + target["Port"] = port_; + target["Manufacturer"] = EnumerationToString(manufacturer_); + } + + + RemoteModalityParameters::RemoteModalityParameters(const Json::Value& serialized) + { + aet_ = SerializationToolbox::ReadString(serialized, "AET"); + host_ = SerializationToolbox::ReadString(serialized, "Host"); + port_ = static_cast<uint16_t> + (SerializationToolbox::ReadUnsignedInteger(serialized, "Port")); + manufacturer_ = StringToModalityManufacturer + (SerializationToolbox::ReadString(serialized, "Manufacturer")); + } }
--- a/Core/DicomNetworking/RemoteModalityParameters.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Mon Jun 11 16:30:13 2018 +0200 @@ -52,6 +52,8 @@ public: RemoteModalityParameters(); + RemoteModalityParameters(const Json::Value& serialized); + RemoteModalityParameters(const std::string& aet, const std::string& host, uint16_t port, @@ -105,5 +107,7 @@ void FromJson(const Json::Value& modality); void ToJson(Json::Value& value) const; + + void Serialize(Json::Value& target) const; }; }
--- a/Core/DicomNetworking/ReusableDicomUserConnection.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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 "../PrecompiledHeaders.h" -#include "ReusableDicomUserConnection.h" - -#include "../Logging.h" -#include "../OrthancException.h" - -namespace Orthanc -{ - static boost::posix_time::ptime Now() - { - return boost::posix_time::microsec_clock::local_time(); - } - - void ReusableDicomUserConnection::Open(const std::string& localAet, - const RemoteModalityParameters& remote) - { - if (connection_ != NULL && - connection_->GetLocalApplicationEntityTitle() == localAet && - connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() && - connection_->GetRemoteHost() == remote.GetHost() && - connection_->GetRemotePort() == remote.GetPort() && - connection_->GetRemoteManufacturer() == remote.GetManufacturer()) - { - // The current connection can be reused - LOG(INFO) << "Reusing the previous SCU connection"; - return; - } - - Close(); - - connection_ = new DicomUserConnection(); - connection_->SetLocalApplicationEntityTitle(localAet); - connection_->SetRemoteModality(remote); - connection_->Open(); - } - - void ReusableDicomUserConnection::Close() - { - if (connection_ != NULL) - { - delete connection_; - connection_ = NULL; - } - } - - void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that) - { - for (;;) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - if (!that->continue_) - { - //LOG(INFO) << "Finishing the thread watching the global SCU connection"; - return; - } - - { - boost::mutex::scoped_lock lock(that->mutex_); - if (that->connection_ != NULL && - Now() >= that->lastUse_ + that->timeBeforeClose_) - { - LOG(INFO) << "Closing the global SCU connection after timeout"; - that->Close(); - } - } - } - } - - - ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote) : - ::Orthanc::Locker(that) - { - that.Open(localAet, remote); - connection_ = that.connection_; - } - - - DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection() - { - if (connection_ == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - return *connection_; - } - - ReusableDicomUserConnection::ReusableDicomUserConnection() : - connection_(NULL), - timeBeforeClose_(boost::posix_time::seconds(5)) // By default, close connection after 5 seconds - { - lastUse_ = Now(); - continue_ = true; - closeThread_ = boost::thread(CloseThread, this); - } - - ReusableDicomUserConnection::~ReusableDicomUserConnection() - { - if (continue_) - { - LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Finalize(); - } - } - - void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms) - { - boost::mutex::scoped_lock lock(mutex_); - - if (ms == 0) - { - ms = 1; - } - - timeBeforeClose_ = boost::posix_time::milliseconds(ms); - } - - void ReusableDicomUserConnection::Lock() - { - mutex_.lock(); - } - - void ReusableDicomUserConnection::Unlock() - { - if (connection_ != NULL && - connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp) - { - // "storescp" from DCMTK has problems when reusing a - // connection. Always close. - Close(); - } - - lastUse_ = Now(); - mutex_.unlock(); - } - - - void ReusableDicomUserConnection::Finalize() - { - if (continue_) - { - continue_ = false; - - if (closeThread_.joinable()) - { - closeThread_.join(); - } - - Close(); - } - } -} -
--- a/Core/DicomNetworking/ReusableDicomUserConnection.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "DicomUserConnection.h" -#include "../../Core/MultiThreading/Locker.h" - -#include <boost/thread.hpp> -#include <boost/date_time/posix_time/posix_time.hpp> - -namespace Orthanc -{ - class ReusableDicomUserConnection : public ILockable - { - private: - boost::mutex mutex_; - DicomUserConnection* connection_; - bool continue_; - boost::posix_time::time_duration timeBeforeClose_; - boost::posix_time::ptime lastUse_; - boost::thread closeThread_; - - void Open(const std::string& localAet, - const RemoteModalityParameters& remote); - - void Close(); - - static void CloseThread(ReusableDicomUserConnection* that); - - protected: - virtual void Lock(); - - virtual void Unlock(); - - public: - class Locker : public ::Orthanc::Locker - { - private: - DicomUserConnection* connection_; - - public: - Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote); - - DicomUserConnection& GetConnection(); - }; - - ReusableDicomUserConnection(); - - virtual ~ReusableDicomUserConnection(); - - void SetMillisecondsBeforeClose(uint64_t ms); - - void Finalize(); - }; -} -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,134 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "TimeoutDicomConnectionManager.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +namespace Orthanc +{ + static boost::posix_time::ptime GetNow() + { + return boost::posix_time::microsec_clock::universal_time(); + } + + class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource + { + private: + TimeoutDicomConnectionManager& that_; + + public: + Resource(TimeoutDicomConnectionManager& that) : + that_(that) + { + if (that_.connection_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + ~Resource() + { + that_.Touch(); + } + + DicomUserConnection& GetConnection() + { + assert(that_.connection_.get() != NULL); + return *that_.connection_; + } + }; + + + void TimeoutDicomConnectionManager::Touch() + { + lastUse_ = GetNow(); + } + + + void TimeoutDicomConnectionManager::CheckTimeoutInternal() + { + if (connection_.get() != NULL && + (GetNow() - lastUse_) >= timeout_) + { + Close(); + } + } + + + void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout) + { + timeout_ = boost::posix_time::milliseconds(timeout); + CheckTimeoutInternal(); + } + + + unsigned int TimeoutDicomConnectionManager::GetTimeout() + { + return timeout_.total_milliseconds(); + } + + + void TimeoutDicomConnectionManager::Close() + { + if (connection_.get() != NULL) + { + LOG(INFO) << "Closing inactive DICOM association with modality: " + << connection_->GetRemoteApplicationEntityTitle(); + + connection_.reset(NULL); + } + } + + + void TimeoutDicomConnectionManager::CheckTimeout() + { + CheckTimeoutInternal(); + } + + + IDicomConnectionManager::IResource* + TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote) + { + if (connection_.get() == NULL || + !connection_->IsSameAssociation(localAet, remote)) + { + connection_.reset(new DicomUserConnection(localAet, remote)); + } + + return new Resource(*this); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IDicomConnectionManager.h" + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 + +namespace Orthanc +{ + class TimeoutDicomConnectionManager : public IDicomConnectionManager + { + public: + void SetTimeout(unsigned int timeout) + { + } + + unsigned int GetTimeout() + { + return 0; + } + + void Close() + { + } + + void CheckTimeout() + { + } + }; +} + +#else + +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace Orthanc +{ + class TimeoutDicomConnectionManager : public IDicomConnectionManager + { + private: + class Resource; + + std::auto_ptr<DicomUserConnection> connection_; + boost::posix_time::ptime lastUse_; + boost::posix_time::time_duration timeout_; + + void Touch(); + + void CheckTimeoutInternal(); + + public: + TimeoutDicomConnectionManager() : + timeout_(boost::posix_time::milliseconds(1000)) + { + } + + void SetTimeout(unsigned int timeout); + + unsigned int GetTimeout(); + + void Close(); + + void CheckTimeout(); + + virtual IResource* AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote); + }; +} + +#endif
--- a/Core/DicomParsing/DicomModification.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomParsing/DicomModification.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -36,6 +36,7 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "FromDcmtkBridge.h" #include "ITagVisitor.h" @@ -1237,4 +1238,166 @@ patientNameReplaced = (IsReplaced(DICOM_TAG_PATIENT_NAME) && GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName); } + + + + + static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags"; + static const char* LEVEL = "Level"; + static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers"; + static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID"; + static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID"; + static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships"; + static const char* REMOVALS = "Removals"; + static const char* CLEARINGS = "Clearings"; + static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep"; + static const char* REPLACEMENTS = "Replacements"; + static const char* MAP_PATIENTS = "MapPatients"; + static const char* MAP_STUDIES = "MapStudies"; + static const char* MAP_SERIES = "MapSeries"; + static const char* MAP_INSTANCES = "MapInstances"; + + void DicomModification::Serialize(Json::Value& value) const + { + if (identifierGenerator_ != NULL) + { + LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator"; + throw OrthancException(ErrorCode_InternalError); + } + + value = Json::objectValue; + value[REMOVE_PRIVATE_TAGS] = removePrivateTags_; + value[LEVEL] = EnumerationToString(level_); + value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_; + value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_; + value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_; + value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_; + + SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS); + SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS); + SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP); + + Json::Value& tmp = value[REPLACEMENTS]; + + tmp = Json::objectValue; + + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + assert(it->second != NULL); + tmp[it->first.Format()] = *it->second; + } + + Json::Value& mapPatients = value[MAP_PATIENTS]; + Json::Value& mapStudies = value[MAP_STUDIES]; + Json::Value& mapSeries = value[MAP_SERIES]; + Json::Value& mapInstances = value[MAP_INSTANCES]; + + mapPatients = Json::objectValue; + mapStudies = Json::objectValue; + mapSeries = Json::objectValue; + mapInstances = Json::objectValue; + + for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it) + { + Json::Value* tmp = NULL; + + switch (it->first.first) + { + case ResourceType_Patient: + tmp = &mapPatients; + break; + + case ResourceType_Study: + tmp = &mapStudies; + break; + + case ResourceType_Series: + tmp = &mapSeries; + break; + + case ResourceType_Instance: + tmp = &mapInstances; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + assert(tmp != NULL); + (*tmp) [it->first.second] = it->second; + } + } + + + void DicomModification::UnserializeUidMap(ResourceType level, + const Json::Value& serialized, + const char* field) + { + if (!serialized.isMember(field) || + serialized[field].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members names = serialized[field].getMemberNames(); + + for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it) + { + const Json::Value& value = serialized[field][*it]; + + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + uidMap_[std::make_pair(level, *it)] = value.asString(); + } + } + } + + + DicomModification::DicomModification(const Json::Value& serialized) : + identifierGenerator_(NULL) + { + removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS); + level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str()); + allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS); + keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID); + keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID); + updateReferencedRelationships_ = SerializationToolbox::ReadBoolean + (serialized, UPDATE_REFERENCED_RELATIONSHIPS); + + SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); + SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS); + SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP); + + if (!serialized.isMember(REPLACEMENTS) || + serialized[REPLACEMENTS].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames(); + + for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it) + { + DicomTag tag(0, 0); + if (!DicomTag::ParseHexadecimal(tag, it->c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + const Json::Value& value = serialized[REPLACEMENTS][*it]; + replacements_.insert(std::make_pair(tag, new Json::Value(value))); + } + } + + UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS); + UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES); + UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES); + UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES); + } }
--- a/Core/DicomParsing/DicomModification.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomParsing/DicomModification.h Mon Jun 11 16:30:13 2018 +0200 @@ -107,9 +107,15 @@ void SetupAnonymization2017c(); + void UnserializeUidMap(ResourceType level, + const Json::Value& serialized, + const char* field); + public: DicomModification(); + DicomModification(const Json::Value& serialized); + ~DicomModification(); void Keep(const DicomTag& tag); @@ -172,5 +178,7 @@ { identifierGenerator_ = &generator; } + + void Serialize(Json::Value& value) const; }; }
--- a/Core/DicomParsing/FromDcmtkBridge.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -106,27 +106,6 @@ namespace Orthanc { - static inline uint16_t GetCharValue(char c) - { - if (c >= '0' && c <= '9') - return c - '0'; - else if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - else - return 0; - } - - static inline uint16_t GetTagValue(const char* c) - { - return ((GetCharValue(c[0]) << 12) + - (GetCharValue(c[1]) << 8) + - (GetCharValue(c[2]) << 4) + - GetCharValue(c[3])); - } - - #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary, EmbeddedResources::FileResourceId resource) @@ -1061,35 +1040,10 @@ DicomTag FromDcmtkBridge::ParseTag(const char* name) { - if (strlen(name) == 9 && - isxdigit(name[0]) && - isxdigit(name[1]) && - isxdigit(name[2]) && - isxdigit(name[3]) && - (name[4] == '-' || name[4] == ',') && - isxdigit(name[5]) && - isxdigit(name[6]) && - isxdigit(name[7]) && - isxdigit(name[8])) + DicomTag parsed(0, 0); + if (DicomTag::ParseHexadecimal(parsed, name)) { - uint16_t group = GetTagValue(name); - uint16_t element = GetTagValue(name + 5); - return DicomTag(group, element); - } - - if (strlen(name) == 8 && - isxdigit(name[0]) && - isxdigit(name[1]) && - isxdigit(name[2]) && - isxdigit(name[3]) && - isxdigit(name[4]) && - isxdigit(name[5]) && - isxdigit(name[6]) && - isxdigit(name[7])) - { - uint16_t group = GetTagValue(name); - uint16_t element = GetTagValue(name + 4); - return DicomTag(group, element); + return parsed; } #if 0
--- a/Core/Enumerations.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/Enumerations.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -164,6 +164,9 @@ case ErrorCode_DatabaseUnavailable: return "The database is currently not available (probably a transient situation)"; + case ErrorCode_CanceledJob: + return "This job was canceled"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -987,6 +990,34 @@ } + const char* EnumerationToString(JobState state) + { + switch (state) + { + case JobState_Pending: + return "Pending"; + + case JobState_Running: + return "Running"; + + case JobState_Success: + return "Success"; + + case JobState_Failure: + return "Failure"; + + case JobState_Paused: + return "Paused"; + + case JobState_Retry: + return "Retry"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + Encoding StringToEncoding(const char* encoding) { std::string s(encoding); @@ -1436,6 +1467,68 @@ } + JobState StringToJobState(const std::string& state) + { + if (state == "Pending") + { + return JobState_Pending; + } + else if (state == "Running") + { + return JobState_Running; + } + else if (state == "Success") + { + return JobState_Success; + } + else if (state == "Failure") + { + return JobState_Failure; + } + else if (state == "Paused") + { + return JobState_Paused; + } + else if (state == "Retry") + { + return JobState_Retry; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + RequestOrigin StringToRequestOrigin(const std::string& origin) + { + if (origin == "Unknown") + { + return RequestOrigin_Unknown; + } + else if (origin == "DicomProtocol") + { + return RequestOrigin_DicomProtocol; + } + else if (origin == "RestApi") + { + return RequestOrigin_RestApi; + } + else if (origin == "Plugins") + { + return RequestOrigin_Plugins; + } + else if (origin == "Lua") + { + return RequestOrigin_Lua; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + unsigned int GetBytesPerPixel(PixelFormat format) { switch (format)
--- a/Core/Enumerations.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/Enumerations.h Mon Jun 11 16:30:13 2018 +0200 @@ -96,6 +96,7 @@ ErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, ErrorCode_NullPointer = 35 /*!< Cannot handle a NULL pointer */, ErrorCode_DatabaseUnavailable = 36 /*!< The database is currently not available (probably a transient situation) */, + ErrorCode_CanceledJob = 37 /*!< This job was canceled */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -548,6 +549,24 @@ TransferSyntax_Rle }; + enum JobState + { + JobState_Pending, + JobState_Running, + JobState_Success, + JobState_Failure, + JobState_Paused, + JobState_Retry + }; + + enum JobStepCode + { + JobStepCode_Success, + JobStepCode_Failure, + JobStepCode_Continue, + JobStepCode_Retry + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -628,6 +647,8 @@ const char* EnumerationToString(ValueRepresentation vr); + const char* EnumerationToString(JobState state); + Encoding StringToEncoding(const char* encoding); ResourceType StringToResourceType(const char* type); @@ -644,6 +665,10 @@ ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); DicomVersion StringToDicomVersion(const std::string& version); + + JobState StringToJobState(const std::string& state); + + RequestOrigin StringToRequestOrigin(const std::string& origin); unsigned int GetBytesPerPixel(PixelFormat format);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/MemoryStorageArea.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,119 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "MemoryStorageArea.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + MemoryStorageArea::~MemoryStorageArea() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + } + + void MemoryStorageArea::Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type) + { + boost::mutex::scoped_lock lock(mutex_); + + if (size != 0 && + content == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (content_.find(uuid) != content_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + content_[uuid] = new std::string(reinterpret_cast<const char*>(content), size); + } + } + + + void MemoryStorageArea::Read(std::string& content, + const std::string& uuid, + FileContentType type) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::const_iterator found = content_.find(uuid); + + if (found == content_.end()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + else if (found->second == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + content.assign(*found->second); + } + } + + + void MemoryStorageArea::Remove(const std::string& uuid, + FileContentType type) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::iterator found = content_.find(uuid); + + if (found == content_.end()) + { + // Ignore second removal + } + else if (found->second == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + delete found->second; + content_.erase(found); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/MemoryStorageArea.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IStorageArea.h" + +#include <boost/thread/mutex.hpp> +#include <map> + +namespace Orthanc +{ + class MemoryStorageArea : public IStorageArea + { + private: + typedef std::map<std::string, std::string*> Content; + + boost::mutex mutex_; + Content content_; + + public: + virtual ~MemoryStorageArea(); + + virtual void Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type); + + virtual void Read(std::string& content, + const std::string& uuid, + FileContentType type); + + virtual void Remove(const std::string& uuid, + FileContentType type); + }; +}
--- a/Core/HttpServer/IIncomingHttpRequestFilter.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/HttpServer/IIncomingHttpRequestFilter.h Mon Jun 11 16:30:13 2018 +0200 @@ -49,6 +49,6 @@ const char* ip, const char* username, const IHttpHandler::Arguments& httpHeaders, - const IHttpHandler::GetArguments& getArguments) const = 0; + const IHttpHandler::GetArguments& getArguments) = 0; }; }
--- a/Core/HttpServer/MongooseServer.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -676,7 +676,7 @@ std::string username = GetAuthenticatedUsername(headers); - const IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); + IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); if (filter != NULL) { if (!filter->IsAllowed(method, request->uri, remoteIp,
--- a/Core/HttpServer/MongooseServer.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/HttpServer/MongooseServer.h Mon Jun 11 16:30:13 2018 +0200 @@ -162,7 +162,7 @@ void SetHttpCompressionEnabled(bool enabled); - const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const + IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const { return filter_; }
--- a/Core/Images/ImageProcessing.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/Images/ImageProcessing.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -550,7 +550,8 @@ return; } - if (target.GetFormat() == PixelFormat_RGBA32 && + if ((target.GetFormat() == PixelFormat_RGBA32 || + target.GetFormat() == PixelFormat_BGRA32) && source.GetFormat() == PixelFormat_Grayscale8) { for (unsigned int y = 0; y < source.GetHeight(); y++)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/GenericJobUnserializer.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "GenericJobUnserializer.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include "Operations/LogJobOperation.h" +#include "Operations/NullOperationValue.h" +#include "Operations/SequenceOfOperationsJob.h" +#include "Operations/StringOperationValue.h" + +namespace Orthanc +{ + IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "SequenceOfOperations") + { + return new SequenceOfOperationsJob(*this, source); + } + else + { + LOG(ERROR) << "Cannot unserialize job of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "Log") + { + return new LogJobOperation; + } + else + { + LOG(ERROR) << "Cannot unserialize operation of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + JobOperationValue* GenericJobUnserializer::UnserializeValue(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "String") + { + return new StringOperationValue(SerializationToolbox::ReadString(source, "Content")); + } + else if (type == "Null") + { + return new NullOperationValue; + } + else + { + LOG(ERROR) << "Cannot unserialize value of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/GenericJobUnserializer.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IJobUnserializer.h" + +namespace Orthanc +{ + class GenericJobUnserializer : public IJobUnserializer + { + public: + virtual IJob* UnserializeJob(const Json::Value& value); + + virtual IJobOperation* UnserializeOperation(const Json::Value& value); + + virtual JobOperationValue* UnserializeValue(const Json::Value& value); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/IJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,68 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobStepResult.h" + +#include <boost/noncopyable.hpp> +#include <json/value.h> + +namespace Orthanc +{ + class IJob : public boost::noncopyable + { + public: + virtual ~IJob() + { + } + + // Method called once the job enters the jobs engine + virtual void Start() = 0; + + virtual JobStepResult ExecuteStep() = 0; + + // Method called once the job is resubmitted after a failure + virtual void SignalResubmit() = 0; + + virtual void ReleaseResources() = 0; // For pausing/canceling jobs + + virtual float GetProgress() = 0; + + virtual void GetJobType(std::string& target) = 0; + + virtual void GetPublicContent(Json::Value& value) = 0; + + virtual bool Serialize(Json::Value& value) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/IJobUnserializer.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IJob.h" +#include "Operations/JobOperationValue.h" +#include "Operations/IJobOperation.h" + +#include <vector> + +namespace Orthanc +{ + class IJobUnserializer : public boost::noncopyable + { + public: + virtual ~IJobUnserializer() + { + } + + virtual IJob* UnserializeJob(const Json::Value& value) = 0; + + virtual IJobOperation* UnserializeOperation(const Json::Value& value) = 0; + + virtual JobOperationValue* UnserializeValue(const Json::Value& value) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobInfo.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,148 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "JobInfo.h" + +#include "../OrthancException.h" + +// This "include" is mandatory for Release builds using Linux Standard Base +#include <boost/math/special_functions/round.hpp> + +namespace Orthanc +{ + JobInfo::JobInfo(const std::string& id, + int priority, + JobState state, + const JobStatus& status, + const boost::posix_time::ptime& creationTime, + const boost::posix_time::ptime& lastStateChangeTime, + const boost::posix_time::time_duration& runtime) : + id_(id), + priority_(priority), + state_(state), + timestamp_(boost::posix_time::microsec_clock::universal_time()), + creationTime_(creationTime), + lastStateChangeTime_(lastStateChangeTime), + runtime_(runtime), + hasEta_(false), + status_(status) + { + if (state_ == JobState_Running) + { + float ms = static_cast<float>(runtime_.total_milliseconds()); + + if (status_.GetProgress() > 0.01f && + ms > 0.01f) + { + float ratio = static_cast<float>(1.0 - status_.GetProgress()); + long long remaining = boost::math::llround(ratio * ms); + eta_ = timestamp_ + boost::posix_time::milliseconds(remaining); + hasEta_ = true; + } + } + } + + + JobInfo::JobInfo() : + priority_(0), + state_(JobState_Failure), + timestamp_(boost::posix_time::microsec_clock::universal_time()), + creationTime_(timestamp_), + lastStateChangeTime_(timestamp_), + runtime_(boost::posix_time::milliseconds(0)), + hasEta_(false) + { + } + + + bool JobInfo::HasCompletionTime() const + { + return (state_ == JobState_Success || + state_ == JobState_Failure); + } + + + const boost::posix_time::ptime& JobInfo::GetEstimatedTimeOfArrival() const + { + if (hasEta_) + { + return eta_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const boost::posix_time::ptime& JobInfo::GetCompletionTime() const + { + if (HasCompletionTime()) + { + return lastStateChangeTime_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void JobInfo::Format(Json::Value& target) const + { + target = Json::objectValue; + target["ID"] = id_; + target["Priority"] = priority_; + target["ErrorCode"] = static_cast<int>(status_.GetErrorCode()); + target["ErrorDescription"] = EnumerationToString(status_.GetErrorCode()); + target["State"] = EnumerationToString(state_); + target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_); + target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_); + target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0; + target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f); + + target["Type"] = status_.GetJobType(); + target["Content"] = status_.GetPublicContent(); + + if (HasEstimatedTimeOfArrival()) + { + target["EstimatedTimeOfArrival"] = boost::posix_time::to_iso_string(GetEstimatedTimeOfArrival()); + } + + if (HasCompletionTime()) + { + target["CompletionTime"] = boost::posix_time::to_iso_string(GetCompletionTime()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobInfo.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,120 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobStatus.h" + +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace Orthanc +{ + class JobInfo + { + private: + std::string id_; + int priority_; + JobState state_; + boost::posix_time::ptime timestamp_; + boost::posix_time::ptime creationTime_; + boost::posix_time::ptime lastStateChangeTime_; + boost::posix_time::time_duration runtime_; + bool hasEta_; + boost::posix_time::ptime eta_; + JobStatus status_; + + public: + JobInfo(const std::string& id, + int priority, + JobState state, + const JobStatus& status, + const boost::posix_time::ptime& creationTime, + const boost::posix_time::ptime& lastStateChangeTime, + const boost::posix_time::time_duration& runtime); + + JobInfo(); + + const std::string& GetIdentifier() const + { + return id_; + } + + int GetPriority() const + { + return priority_; + } + + JobState GetState() const + { + return state_; + } + + const boost::posix_time::ptime& GetInfoTime() const + { + return timestamp_; + } + + const boost::posix_time::ptime& GetCreationTime() const + { + return creationTime_; + } + + const boost::posix_time::time_duration& GetRuntime() const + { + return runtime_; + } + + bool HasEstimatedTimeOfArrival() const + { + return hasEta_; + } + + bool HasCompletionTime() const; + + const boost::posix_time::ptime& GetEstimatedTimeOfArrival() const; + + const boost::posix_time::ptime& GetCompletionTime() const; + + const JobStatus& GetStatus() const + { + return status_; + } + + JobStatus& GetStatus() + { + return status_; + } + + void Format(Json::Value& target) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStatus.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "JobStatus.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStatus::JobStatus() : + errorCode_(ErrorCode_InternalError), + progress_(0), + jobType_("Invalid"), + publicContent_(Json::objectValue), + hasSerialized_(false) + { + } + + + JobStatus::JobStatus(ErrorCode code, + IJob& job) : + errorCode_(code), + progress_(job.GetProgress()), + publicContent_(Json::objectValue) + { + if (progress_ < 0) + { + progress_ = 0; + } + + if (progress_ > 1) + { + progress_ = 1; + } + + job.GetJobType(jobType_); + job.GetPublicContent(publicContent_); + + hasSerialized_ = job.Serialize(serialized_); + } + + + const Json::Value& JobStatus::GetSerialized() const + { + if (!hasSerialized_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return serialized_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStatus.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,88 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IJob.h" + +namespace Orthanc +{ + class JobStatus + { + private: + ErrorCode errorCode_; + float progress_; + std::string jobType_; + Json::Value publicContent_; + Json::Value serialized_; + bool hasSerialized_; + + public: + JobStatus(); + + JobStatus(ErrorCode code, + IJob& job); + + ErrorCode GetErrorCode() const + { + return errorCode_; + } + + void SetErrorCode(ErrorCode error) + { + errorCode_ = error; + } + + float GetProgress() const + { + return progress_; + } + + const std::string& GetJobType() const + { + return jobType_; + } + + const Json::Value& GetPublicContent() const + { + return publicContent_; + } + + const Json::Value& GetSerialized() const; + + bool HasSerialized() const + { + return hasSerialized_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStepResult.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "JobStepResult.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStepResult JobStepResult::Retry(unsigned int timeout) + { + JobStepResult result(JobStepCode_Retry); + result.timeout_ = timeout; + return result; + } + + + JobStepResult JobStepResult::Failure(const ErrorCode& error) + { + JobStepResult result(JobStepCode_Failure); + result.error_ = error; + return result; + } + + + unsigned int JobStepResult::GetRetryTimeout() const + { + if (code_ == JobStepCode_Retry) + { + return timeout_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ErrorCode JobStepResult::GetFailureCode() const + { + if (code_ == JobStepCode_Failure) + { + return error_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStepResult.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../Enumerations.h" + +namespace Orthanc +{ + class JobStepResult + { + private: + JobStepCode code_; + unsigned int timeout_; + ErrorCode error_; + + explicit JobStepResult(JobStepCode code) : + code_(code), + timeout_(0), + error_(ErrorCode_Success) + { + } + + public: + explicit JobStepResult() : + code_(JobStepCode_Failure), + timeout_(0), + error_(ErrorCode_InternalError) + { + } + + static JobStepResult Success() + { + return JobStepResult(JobStepCode_Success); + } + + static JobStepResult Continue() + { + return JobStepResult(JobStepCode_Continue); + } + + static JobStepResult Retry(unsigned int timeout); + + static JobStepResult Failure(const ErrorCode& error); + + JobStepCode GetCode() const + { + return code_; + } + + unsigned int GetRetryTimeout() const; + + ErrorCode GetFailureCode() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsEngine.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,324 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "JobsEngine.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +#include <json/reader.h> + +namespace Orthanc +{ + bool JobsEngine::IsRunning() + { + boost::mutex::scoped_lock lock(stateMutex_); + return (state_ == State_Running); + } + + + bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex) + { + assert(running.IsValid()); + + if (running.IsPauseScheduled()) + { + running.GetJob().ReleaseResources(); + running.MarkPause(); + return false; + } + + if (running.IsCancelScheduled()) + { + running.GetJob().ReleaseResources(); + running.MarkCanceled(); + return false; + } + + JobStepResult result; + + try + { + result = running.GetJob().ExecuteStep(); + } + catch (OrthancException& e) + { + result = JobStepResult::Failure(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + result = JobStepResult::Failure(ErrorCode_BadFileFormat); + } + catch (...) + { + result = JobStepResult::Failure(ErrorCode_InternalError); + } + + switch (result.GetCode()) + { + case JobStepCode_Success: + running.UpdateStatus(ErrorCode_Success); + running.GetJob().ReleaseResources(); + running.MarkSuccess(); + return false; + + case JobStepCode_Failure: + running.GetJob().ReleaseResources(); + running.UpdateStatus(result.GetFailureCode()); + running.MarkFailure(); + return false; + + case JobStepCode_Retry: + running.GetJob().ReleaseResources(); + running.UpdateStatus(ErrorCode_Success); + running.MarkRetry(result.GetRetryTimeout()); + return false; + + case JobStepCode_Continue: + running.UpdateStatus(ErrorCode_Success); + return true; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void JobsEngine::RetryHandler(JobsEngine* engine) + { + assert(engine != NULL); + + while (engine->IsRunning()) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(engine->threadSleep_)); + engine->GetRegistry().ScheduleRetries(); + } + } + + + void JobsEngine::Worker(JobsEngine* engine, + size_t workerIndex) + { + assert(engine != NULL); + + LOG(INFO) << "Worker thread " << workerIndex << " has started"; + + while (engine->IsRunning()) + { + JobsRegistry::RunningJob running(engine->GetRegistry(), engine->threadSleep_); + + if (running.IsValid()) + { + LOG(INFO) << "Executing job with priority " << running.GetPriority() + << " in worker thread " << workerIndex << ": " << running.GetId(); + + while (engine->IsRunning()) + { + if (!engine->ExecuteStep(running, workerIndex)) + { + break; + } + } + } + } + } + + + JobsEngine::JobsEngine() : + state_(State_Setup), + registry_(new JobsRegistry), + threadSleep_(200), + workers_(1) + { + } + + + JobsEngine::~JobsEngine() + { + if (state_ != State_Setup && + state_ != State_Done) + { + LOG(ERROR) << "INTERNAL ERROR: JobsEngine::Stop() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } + } + + + JobsRegistry& JobsEngine::GetRegistry() + { + if (registry_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *registry_; + } + + + void JobsEngine::LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + registry_.reset(new JobsRegistry(unserializer, serialized)); + } + + + void JobsEngine::LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized) + { + Json::Value value; + Json::Reader reader; + if (reader.parse(serialized, value)) + { + LoadRegistryFromJson(unserializer, value); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void JobsEngine::SetWorkersCount(size_t count) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + workers_.resize(count); + } + + + void JobsEngine::SetThreadSleep(unsigned int sleep) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + threadSleep_ = sleep; + } + + + void JobsEngine::Start() + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + retryHandler_ = boost::thread(RetryHandler, this); + + if (workers_.size() == 0) + { + // Use all the available CPUs + size_t n = boost::thread::hardware_concurrency(); + + if (n == 0) + { + n = 1; + } + + workers_.resize(n); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] == NULL); + workers_[i] = new boost::thread(Worker, this, i); + } + + state_ = State_Running; + + LOG(WARNING) << "The jobs engine has started with " << workers_.size() << " threads"; + } + + + void JobsEngine::Stop() + { + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Running) + { + return; + } + + state_ = State_Stopping; + } + + LOG(INFO) << "Stopping the jobs engine"; + + if (retryHandler_.joinable()) + { + retryHandler_.join(); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] != NULL); + + if (workers_[i]->joinable()) + { + workers_[i]->join(); + } + + delete workers_[i]; + } + + { + boost::mutex::scoped_lock lock(stateMutex_); + state_ = State_Done; + } + + LOG(WARNING) << "The jobs engine has stopped"; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsEngine.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,91 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobsRegistry.h" + +#include <boost/thread.hpp> + +namespace Orthanc +{ + class JobsEngine + { + private: + enum State + { + State_Setup, + State_Running, + State_Stopping, + State_Done + }; + + boost::mutex stateMutex_; + State state_; + std::auto_ptr<JobsRegistry> registry_; + boost::thread retryHandler_; + unsigned int threadSleep_; + std::vector<boost::thread*> workers_; + + bool IsRunning(); + + bool ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex); + + static void RetryHandler(JobsEngine* engine); + + static void Worker(JobsEngine* engine, + size_t workerIndex); + + public: + JobsEngine(); + + ~JobsEngine(); + + JobsRegistry& GetRegistry(); + + void LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized); + + void LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized); + + void SetWorkersCount(size_t count); + + void SetThreadSleep(unsigned int sleep); + + void Start(); + + void Stop(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsRegistry.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,1304 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "JobsRegistry.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../SerializationToolbox.h" + +namespace Orthanc +{ + static const char* STATE = "State"; + static const char* TYPE = "Type"; + static const char* PRIORITY = "Priority"; + static const char* JOB = "Job"; + static const char* JOBS = "Jobs"; + static const char* JOBS_REGISTRY = "JobsRegistry"; + static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs"; + static const char* CREATION_TIME = "CreationTime"; + static const char* LAST_CHANGE_TIME = "LastChangeTime"; + static const char* RUNTIME = "Runtime"; + + + class JobsRegistry::JobHandler : public boost::noncopyable + { + private: + std::string id_; + JobState state_; + std::string jobType_; + std::auto_ptr<IJob> job_; + int priority_; // "+inf()" means highest priority + boost::posix_time::ptime creationTime_; + boost::posix_time::ptime lastStateChangeTime_; + boost::posix_time::time_duration runtime_; + boost::posix_time::ptime retryTime_; + bool pauseScheduled_; + bool cancelScheduled_; + JobStatus lastStatus_; + + void Touch() + { + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + if (state_ == JobState_Running) + { + runtime_ += (now - lastStateChangeTime_); + } + + lastStateChangeTime_ = now; + } + + void SetStateInternal(JobState state) + { + state_ = state; + pauseScheduled_ = false; + cancelScheduled_ = false; + Touch(); + } + + public: + JobHandler(IJob* job, + int priority) : + id_(Toolbox::GenerateUuid()), + state_(JobState_Pending), + job_(job), + priority_(priority), + creationTime_(boost::posix_time::microsec_clock::universal_time()), + lastStateChangeTime_(creationTime_), + runtime_(boost::posix_time::milliseconds(0)), + retryTime_(creationTime_), + pauseScheduled_(false), + cancelScheduled_(false) + { + if (job == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + job->GetJobType(jobType_); + job->Start(); + + lastStatus_ = JobStatus(ErrorCode_Success, *job_); + } + + const std::string& GetId() const + { + return id_; + } + + IJob& GetJob() const + { + assert(job_.get() != NULL); + return *job_; + } + + void SetPriority(int priority) + { + priority_ = priority; + } + + int GetPriority() const + { + return priority_; + } + + JobState GetState() const + { + return state_; + } + + void SetState(JobState state) + { + if (state == JobState_Retry) + { + // Use "SetRetryState()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + SetStateInternal(state); + } + } + + void SetRetryState(unsigned int timeout) + { + if (state_ == JobState_Running) + { + SetStateInternal(JobState_Retry); + retryTime_ = (boost::posix_time::microsec_clock::universal_time() + + boost::posix_time::milliseconds(timeout)); + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void SchedulePause() + { + if (state_ == JobState_Running) + { + pauseScheduled_ = true; + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void ScheduleCancel() + { + if (state_ == JobState_Running) + { + cancelScheduled_ = true; + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + bool IsPauseScheduled() + { + return pauseScheduled_; + } + + bool IsCancelScheduled() + { + return cancelScheduled_; + } + + bool IsRetryReady(const boost::posix_time::ptime& now) const + { + if (state_ != JobState_Retry) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return retryTime_ <= now; + } + } + + const boost::posix_time::ptime& GetCreationTime() const + { + return creationTime_; + } + + const boost::posix_time::ptime& GetLastStateChangeTime() const + { + return lastStateChangeTime_; + } + + void SetLastStateChangeTime(const boost::posix_time::ptime& time) + { + lastStateChangeTime_ = time; + } + + const boost::posix_time::time_duration& GetRuntime() const + { + return runtime_; + } + + const JobStatus& GetLastStatus() const + { + return lastStatus_; + } + + void SetLastStatus(const JobStatus& status) + { + lastStatus_ = status; + Touch(); + } + + void SetLastErrorCode(ErrorCode code) + { + lastStatus_.SetErrorCode(code); + } + + bool Serialize(Json::Value& target) const + { + target = Json::objectValue; + + bool ok; + + if (state_ == JobState_Running) + { + // WARNING: Cannot directly access the "job_" member, as long + // as a "RunningJob" instance is running. We do not use a + // mutex at the "JobHandler" level, as serialization would be + // blocked while a step in the job is running. Instead, we + // save a snapshot of the serialized job. + + if (lastStatus_.HasSerialized()) + { + target[JOB] = lastStatus_.GetSerialized(); + ok = true; + } + else + { + ok = false; + } + } + else + { + ok = job_->Serialize(target[JOB]); + } + + if (ok) + { + target[STATE] = EnumerationToString(state_); + target[PRIORITY] = priority_; + target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_); + target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_); + target[RUNTIME] = static_cast<unsigned int>(runtime_.total_milliseconds()); + return true; + } + else + { + LOG(INFO) << "Job backup is not supported for job of type: " << jobType_; + return false; + } + } + + JobHandler(IJobUnserializer& unserializer, + const Json::Value& serialized, + const std::string& id) : + id_(id), + pauseScheduled_(false), + cancelScheduled_(false) + { + state_ = StringToJobState(SerializationToolbox::ReadString(serialized, STATE)); + priority_ = SerializationToolbox::ReadInteger(serialized, PRIORITY); + creationTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, CREATION_TIME)); + lastStateChangeTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, LAST_CHANGE_TIME)); + runtime_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadInteger(serialized, RUNTIME)); + + retryTime_ = creationTime_; + + job_.reset(unserializer.UnserializeJob(serialized[JOB])); + job_->GetJobType(jobType_); + job_->Start(); + + lastStatus_ = JobStatus(ErrorCode_Success, *job_); + } + }; + + + bool JobsRegistry::PriorityComparator::operator() (JobHandler*& a, + JobHandler*& b) const + { + return a->GetPriority() < b->GetPriority(); + } + + +#if defined(NDEBUG) + void JobsRegistry::CheckInvariants() const + { + } + +#else + bool JobsRegistry::IsPendingJob(const JobHandler& job) const + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + if (copy.top() == &job) + { + return true; + } + + copy.pop(); + } + + return false; + } + + bool JobsRegistry::IsCompletedJob(JobHandler& job) const + { + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == &job) + { + return true; + } + } + + return false; + } + + bool JobsRegistry::IsRetryJob(JobHandler& job) const + { + return retryJobs_.find(&job) != retryJobs_.end(); + } + + void JobsRegistry::CheckInvariants() const + { + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + assert(copy.top()->GetState() == JobState_Pending); + copy.pop(); + } + } + + assert(completedJobs_.size() <= maxCompletedJobs_); + + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Success || + (*it)->GetState() == JobState_Failure); + } + + for (RetryJobs::const_iterator it = retryJobs_.begin(); + it != retryJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Retry); + } + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + JobHandler& job = *it->second; + + assert(job.GetId() == it->first); + + switch (job.GetState()) + { + case JobState_Pending: + assert(!IsRetryJob(job) && IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Success: + case JobState_Failure: + assert(!IsRetryJob(job) && !IsPendingJob(job) && IsCompletedJob(job)); + break; + + case JobState_Retry: + assert(IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Running: + case JobState_Paused: + assert(!IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } +#endif + + + void JobsRegistry::ForgetOldCompletedJobs() + { + if (maxCompletedJobs_ != 0) + { + while (completedJobs_.size() > maxCompletedJobs_) + { + assert(completedJobs_.front() != NULL); + + std::string id = completedJobs_.front()->GetId(); + assert(jobsIndex_.find(id) != jobsIndex_.end()); + + jobsIndex_.erase(id); + delete(completedJobs_.front()); + completedJobs_.pop_front(); + } + } + } + + + void JobsRegistry::SetCompletedJob(JobHandler& job, + bool success) + { + job.SetState(success ? JobState_Success : JobState_Failure); + + completedJobs_.push_back(&job); + ForgetOldCompletedJobs(); + + someJobComplete_.notify_all(); + } + + + void JobsRegistry::MarkRunningAsCompleted(JobHandler& job, + bool success) + { + LOG(INFO) << "Job has completed with " << (success ? "success" : "failure") + << ": " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running); + SetCompletedJob(job, success); + + if (observer_ != NULL) + { + if (success) + { + observer_->SignalJobSuccess(job.GetId()); + } + else + { + observer_->SignalJobFailure(job.GetId()); + } + } + + CheckInvariants(); + } + + + void JobsRegistry::MarkRunningAsRetry(JobHandler& job, + unsigned int timeout) + { + LOG(INFO) << "Job scheduled for retry in " << timeout << "ms: " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running && + retryJobs_.find(&job) == retryJobs_.end()); + + retryJobs_.insert(&job); + job.SetRetryState(timeout); + + CheckInvariants(); + } + + + void JobsRegistry::MarkRunningAsPaused(JobHandler& job) + { + LOG(INFO) << "Job paused: " << job.GetId(); + + CheckInvariants(); + assert(job.GetState() == JobState_Running); + + job.SetState(JobState_Paused); + + CheckInvariants(); + } + + + bool JobsRegistry::GetStateInternal(JobState& state, + const std::string& id) + { + CheckInvariants(); + + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it == jobsIndex_.end()) + { + return false; + } + else + { + state = it->second->GetState(); + return true; + } + } + + + JobsRegistry::~JobsRegistry() + { + for (JobsIndex::iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void JobsRegistry::SetMaxCompletedJobs(size_t n) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + LOG(INFO) << "The size of the history of the jobs engine is set to: " << n << " job(s)"; + + maxCompletedJobs_ = n; + ForgetOldCompletedJobs(); + + CheckInvariants(); + } + + + void JobsRegistry::ListJobs(std::set<std::string>& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool JobsRegistry::GetJobInfo(JobInfo& target, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + target = JobInfo(handler.GetId(), + handler.GetPriority(), + handler.GetState(), + handler.GetLastStatus(), + handler.GetCreationTime(), + handler.GetLastStateChangeTime(), + handler.GetRuntime()); + return true; + } + } + + + void JobsRegistry::SubmitInternal(std::string& id, + JobHandler* handlerRaw, + bool keepLastChangeTime) + { + if (handlerRaw == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + std::auto_ptr<JobHandler> handler(handlerRaw); + + boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime(); + + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + id = handler->GetId(); + int priority = handler->GetPriority(); + + switch (handler->GetState()) + { + case JobState_Pending: + case JobState_Retry: + case JobState_Running: + handler->SetState(JobState_Pending); + pendingJobs_.push(handler.get()); + pendingJobAvailable_.notify_one(); + break; + + case JobState_Success: + SetCompletedJob(*handler, true); + break; + + case JobState_Failure: + SetCompletedJob(*handler, false); + break; + + case JobState_Paused: + break; + + default: + LOG(ERROR) << "A job should not be loaded from state: " + << EnumerationToString(handler->GetState()); + throw OrthancException(ErrorCode_InternalError); + } + + if (keepLastChangeTime) + { + handler->SetLastStateChangeTime(lastChangeTime); + } + + jobsIndex_.insert(std::make_pair(id, handler.release())); + + LOG(INFO) << "New job submitted with priority " << priority << ": " << id; + + if (observer_ != NULL) + { + observer_->SignalJobSubmitted(id); + } + + CheckInvariants(); + } + } + + + void JobsRegistry::Submit(std::string& id, + IJob* job, // Takes ownership + int priority) + { + SubmitInternal(id, new JobHandler(job, priority), false); + } + + + void JobsRegistry::Submit(IJob* job, // Takes ownership + int priority) + { + std::string id; + SubmitInternal(id, new JobHandler(job, priority), false); + } + + + bool JobsRegistry::SubmitAndWait(IJob* job, // Takes ownership + int priority) + { + std::string id; + Submit(id, job, priority); + + JobState state; + + { + boost::mutex::scoped_lock lock(mutex_); + + while (GetStateInternal(state, id) && + state != JobState_Success && + state != JobState_Failure) + { + someJobComplete_.wait(lock); + } + } + + return (state == JobState_Success); + } + + + bool JobsRegistry::SetPriority(const std::string& id, + int priority) + { + LOG(INFO) << "Changing priority to " << priority << " for job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + found->second->SetPriority(priority); + + if (found->second->GetState() == JobState_Pending) + { + // If the job is pending, we need to reconstruct the + // priority queue, as the heap condition has changed + + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + pendingJobs_.push(copy.top()); + copy.pop(); + } + } + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::RemovePendingJob(const std::string& id) + { + // If the job is pending, we need to reconstruct the priority + // queue to remove it + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + if (copy.top()->GetId() != id) + { + pendingJobs_.push(copy.top()); + } + + copy.pop(); + } + } + + + void JobsRegistry::RemoveRetryJob(JobHandler* handler) + { + RetryJobs::iterator item = retryJobs_.find(handler); + assert(item != retryJobs_.end()); + retryJobs_.erase(item); + } + + + bool JobsRegistry::Pause(const std::string& id) + { + LOG(INFO) << "Pausing job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + found->second->SetState(JobState_Paused); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + found->second->SetState(JobState_Paused); + break; + + case JobState_Paused: + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->SchedulePause(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Cancel(const std::string& id) + { + LOG(INFO) << "Canceling job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Paused: + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->ScheduleCancel(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Resume(const std::string& id) + { + LOG(INFO) << "Resuming job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Paused) + { + LOG(WARNING) << "Cannot resume a job that is not paused: " << id; + return false; + } + else + { + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Resubmit(const std::string& id) + { + LOG(INFO) << "Resubmitting failed job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Failure) + { + LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id; + return false; + } + else + { + found->second->GetJob().SignalResubmit(); + + bool ok = false; + for (CompletedJobs::iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == found->second) + { + ok = true; + completedJobs_.erase(it); + break; + } + } + + assert(ok); + + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::ScheduleRetries() + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + RetryJobs copy; + std::swap(copy, retryJobs_); + + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + assert(retryJobs_.empty()); + for (RetryJobs::iterator it = copy.begin(); it != copy.end(); ++it) + { + if ((*it)->IsRetryReady(now)) + { + LOG(INFO) << "Retrying job: " << (*it)->GetId(); + (*it)->SetState(JobState_Pending); + pendingJobs_.push(*it); + pendingJobAvailable_.notify_one(); + } + else + { + retryJobs_.insert(*it); + } + } + + CheckInvariants(); + } + + + bool JobsRegistry::GetState(JobState& state, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + return GetStateInternal(state, id); + } + + + void JobsRegistry::SetObserver(JobsRegistry::IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = &observer; + } + + + void JobsRegistry::ResetObserver() + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = NULL; + } + + + JobsRegistry::RunningJob::RunningJob(JobsRegistry& registry, + unsigned int timeout) : + registry_(registry), + handler_(NULL), + targetState_(JobState_Failure), + targetRetryTimeout_(0), + canceled_(false) + { + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + while (registry_.pendingJobs_.empty()) + { + if (timeout == 0) + { + registry_.pendingJobAvailable_.wait(lock); + } + else + { + bool success = registry_.pendingJobAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(timeout)); + if (!success) + { + // No pending job + return; + } + } + } + + handler_ = registry_.pendingJobs_.top(); + registry_.pendingJobs_.pop(); + + assert(handler_->GetState() == JobState_Pending); + handler_->SetState(JobState_Running); + handler_->SetLastErrorCode(ErrorCode_Success); + + job_ = &handler_->GetJob(); + id_ = handler_->GetId(); + priority_ = handler_->GetPriority(); + } + } + + + JobsRegistry::RunningJob::~RunningJob() + { + if (IsValid()) + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + switch (targetState_) + { + case JobState_Failure: + registry_.MarkRunningAsCompleted(*handler_, false); + + if (canceled_) + { + handler_->SetLastErrorCode(ErrorCode_CanceledJob); + } + + break; + + case JobState_Success: + registry_.MarkRunningAsCompleted(*handler_, true); + break; + + case JobState_Paused: + registry_.MarkRunningAsPaused(*handler_); + break; + + case JobState_Retry: + registry_.MarkRunningAsRetry(*handler_, targetRetryTimeout_); + break; + + default: + assert(0); + } + } + } + + + bool JobsRegistry::RunningJob::IsValid() const + { + return (handler_ != NULL && + job_ != NULL); + } + + + const std::string& JobsRegistry::RunningJob::GetId() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return id_; + } + } + + + int JobsRegistry::RunningJob::GetPriority() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return priority_; + } + } + + + IJob& JobsRegistry::RunningJob::GetJob() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *job_; + } + } + + + bool JobsRegistry::RunningJob::IsPauseScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsPauseScheduled(); + } + } + + + bool JobsRegistry::RunningJob::IsCancelScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsCancelScheduled(); + } + } + + + void JobsRegistry::RunningJob::MarkSuccess() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Success; + } + } + + + void JobsRegistry::RunningJob::MarkFailure() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + } + } + + + void JobsRegistry::RunningJob::MarkCanceled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + canceled_ = true; + } + } + + + void JobsRegistry::RunningJob::MarkPause() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Paused; + } + } + + + void JobsRegistry::RunningJob::MarkRetry(unsigned int timeout) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Retry; + targetRetryTimeout_ = timeout; + } + } + + + void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + JobStatus status(code, *job_); + + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + handler_->SetLastStatus(status); + } + } + + + + void JobsRegistry::Serialize(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + target = Json::objectValue; + target[TYPE] = JOBS_REGISTRY; + target[MAX_COMPLETED_JOBS] = static_cast<unsigned int>(maxCompletedJobs_); + target[JOBS] = Json::objectValue; + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + Json::Value v; + if (it->second->Serialize(v)) + { + target[JOBS][it->first] = v; + } + } + } + + + JobsRegistry::JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s) : + observer_(NULL) + { + if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY || + !s.isMember(JOBS) || + s[JOBS].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS); + + Json::Value::Members members = s[JOBS].getMemberNames(); + + for (Json::Value::Members::const_iterator it = members.begin(); + it != members.end(); ++it) + { + std::auto_ptr<JobHandler> job(new JobHandler(unserializer, s[JOBS][*it], *it)); + + std::string id; + SubmitInternal(id, job.release(), true); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsRegistry.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,234 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The job engine cannot be used in sandboxed environments +#endif + +#include "JobInfo.h" +#include "IJobUnserializer.h" + +#include <list> +#include <set> +#include <queue> +#include <boost/thread/mutex.hpp> +#include <boost/thread/condition_variable.hpp> + +namespace Orthanc +{ + // This class handles the state machine of the jobs engine + class JobsRegistry : public boost::noncopyable + { + public: + class IObserver : public boost::noncopyable + { + public: + virtual ~IObserver() + { + } + + virtual void SignalJobSubmitted(const std::string& jobId) = 0; + + virtual void SignalJobSuccess(const std::string& jobId) = 0; + + virtual void SignalJobFailure(const std::string& jobId) = 0; + }; + + private: + class JobHandler; + + struct PriorityComparator + { + bool operator() (JobHandler*& a, + JobHandler*& b) const; + }; + + typedef std::map<std::string, JobHandler*> JobsIndex; + typedef std::list<JobHandler*> CompletedJobs; + typedef std::set<JobHandler*> RetryJobs; + typedef std::priority_queue<JobHandler*, + std::vector<JobHandler*>, // Could be a "std::deque" + PriorityComparator> PendingJobs; + + boost::mutex mutex_; + JobsIndex jobsIndex_; + PendingJobs pendingJobs_; + CompletedJobs completedJobs_; + RetryJobs retryJobs_; + + boost::condition_variable pendingJobAvailable_; + boost::condition_variable someJobComplete_; + size_t maxCompletedJobs_; + + IObserver* observer_; + + +#ifndef NDEBUG + bool IsPendingJob(const JobHandler& job) const; + + bool IsCompletedJob(JobHandler& job) const; + + bool IsRetryJob(JobHandler& job) const; +#endif + + void CheckInvariants() const; + + void ForgetOldCompletedJobs(); + + void SetCompletedJob(JobHandler& job, + bool success); + + void MarkRunningAsCompleted(JobHandler& job, + bool success); + + void MarkRunningAsRetry(JobHandler& job, + unsigned int timeout); + + void MarkRunningAsPaused(JobHandler& job); + + bool GetStateInternal(JobState& state, + const std::string& id); + + void RemovePendingJob(const std::string& id); + + void RemoveRetryJob(JobHandler* handler); + + void SubmitInternal(std::string& id, + JobHandler* handler, + bool keepLastChangeTime); + + public: + JobsRegistry() : + maxCompletedJobs_(10), + observer_(NULL) + { + } + + JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s); + + ~JobsRegistry(); + + void SetMaxCompletedJobs(size_t i); + + void ListJobs(std::set<std::string>& target); + + bool GetJobInfo(JobInfo& target, + const std::string& id); + + void Serialize(Json::Value& target); + + void Submit(std::string& id, + IJob* job, // Takes ownership + int priority); + + void Submit(IJob* job, // Takes ownership + int priority); + + bool SubmitAndWait(IJob* job, // Takes ownership + int priority); + + bool SetPriority(const std::string& id, + int priority); + + bool Pause(const std::string& id); + + bool Resume(const std::string& id); + + bool Resubmit(const std::string& id); + + bool Cancel(const std::string& id); + + void ScheduleRetries(); + + bool GetState(JobState& state, + const std::string& id); + + void SetObserver(IObserver& observer); + + void ResetObserver(); + + class RunningJob : public boost::noncopyable + { + private: + JobsRegistry& registry_; + JobHandler* handler_; // Can only be accessed if the + // registry mutex is locked! + IJob* job_; // Will by design be in mutual exclusion, + // because only one RunningJob can be + // executed at a time on a JobHandler + + std::string id_; + int priority_; + JobState targetState_; + unsigned int targetRetryTimeout_; + bool canceled_; + + public: + RunningJob(JobsRegistry& registry, + unsigned int timeout); + + ~RunningJob(); + + bool IsValid() const; + + const std::string& GetId() const; + + int GetPriority() const; + + IJob& GetJob(); + + bool IsPauseScheduled(); + + bool IsCancelScheduled(); + + void MarkSuccess(); + + void MarkFailure(); + + void MarkPause(); + + void MarkCanceled(); + + void MarkRetry(unsigned int timeout); + + void UpdateStatus(ErrorCode code); + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/IJobOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobOperationValues.h" +#include "../../DicomNetworking/IDicomConnectionManager.h" + +namespace Orthanc +{ + class IJobOperation : public boost::noncopyable + { + public: + virtual ~IJobOperation() + { + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& dicomConnection) = 0; + + virtual void Serialize(Json::Value& result) const = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValue.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,74 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include <json/value.h> +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class JobOperationValue : public boost::noncopyable + { + public: + enum Type + { + Type_DicomInstance, + Type_Null, + Type_String + }; + + private: + Type type_; + + protected: + JobOperationValue(Type type) : + type_(type) + { + } + + public: + virtual ~JobOperationValue() + { + } + + Type GetType() const + { + return type_; + } + + virtual JobOperationValue* Clone() const = 0; + + virtual void Serialize(Json::Value& target) const = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValues.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,143 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../../PrecompiledHeaders.h" +#include "JobOperationValues.h" + +#include "../IJobUnserializer.h" +#include "../../OrthancException.h" + +#include <cassert> +#include <memory> + +namespace Orthanc +{ + void JobOperationValues::Append(JobOperationValues& target, + bool clear) + { + target.Reserve(target.GetSize() + GetSize()); + + for (size_t i = 0; i < values_.size(); i++) + { + if (clear) + { + target.Append(values_[i]); + values_[i] = NULL; + } + else + { + target.Append(GetValue(i).Clone()); + } + } + + if (clear) + { + Clear(); + } + } + + + void JobOperationValues::Clear() + { + for (size_t i = 0; i < values_.size(); i++) + { + if (values_[i] != NULL) + { + delete values_[i]; + } + } + + values_.clear(); + } + + + void JobOperationValues::Append(JobOperationValue* value) // Takes ownership + { + if (value == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + values_.push_back(value); + } + } + + + JobOperationValue& JobOperationValues::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(values_[index] != NULL); + return *values_[index]; + } + } + + + void JobOperationValues::Serialize(Json::Value& target) const + { + target = Json::arrayValue; + + for (size_t i = 0; i < values_.size(); i++) + { + Json::Value tmp; + values_[i]->Serialize(tmp); + target.append(tmp); + } + } + + + JobOperationValues* JobOperationValues::Unserialize(IJobUnserializer& unserializer, + const Json::Value& source) + { + if (source.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::auto_ptr<JobOperationValues> result(new JobOperationValues); + + result->Reserve(source.size()); + + for (Json::Value::ArrayIndex i = 0; i < source.size(); i++) + { + result->Append(unserializer.UnserializeValue(source[i])); + } + + return result.release(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValues.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,89 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobOperationValue.h" + +#include <vector> + +namespace Orthanc +{ + class IJobUnserializer; + + class JobOperationValues : public boost::noncopyable + { + private: + std::vector<JobOperationValue*> values_; + + void Append(JobOperationValues& target, + bool clear); + + public: + ~JobOperationValues() + { + Clear(); + } + + void Move(JobOperationValues& target) + { + return Append(target, true); + } + + void Copy(JobOperationValues& target) + { + return Append(target, false); + } + + void Clear(); + + void Reserve(size_t count) + { + values_.reserve(count); + } + + void Append(JobOperationValue* value); // Takes ownership + + size_t GetSize() const + { + return values_.size(); + } + + JobOperationValue& GetValue(size_t index) const; + + void Serialize(Json::Value& target) const; + + static JobOperationValues* Unserialize(IJobUnserializer& unserializer, + const Json::Value& source); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/LogJobOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../../PrecompiledHeaders.h" +#include "LogJobOperation.h" + +#include "../../Logging.h" +#include "StringOperationValue.h" + +namespace Orthanc +{ + void LogJobOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + switch (input.GetType()) + { + case JobOperationValue::Type_String: + LOG(INFO) << "Job value: " + << dynamic_cast<const StringOperationValue&>(input).GetContent(); + break; + + case JobOperationValue::Type_Null: + LOG(INFO) << "Job value: (null)"; + break; + + default: + LOG(INFO) << "Job value: (unsupport)"; + break; + } + + outputs.Append(input.Clone()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/LogJobOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IJobOperation.h" + +namespace Orthanc +{ + class LogJobOperation : public IJobOperation + { + public: + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "Log"; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/NullOperationValue.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,59 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobOperationValue.h" + +namespace Orthanc +{ + class NullOperationValue : public JobOperationValue + { + public: + NullOperationValue() : + JobOperationValue(Type_Null) + { + } + + virtual JobOperationValue* Clone() const + { + return new NullOperationValue; + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "Null"; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,495 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../../PrecompiledHeaders.h" +#include "SequenceOfOperationsJob.h" + +#include "../../Logging.h" +#include "../../OrthancException.h" +#include "../../SerializationToolbox.h" +#include "../IJobUnserializer.h" + +namespace Orthanc +{ + static const char* CURRENT = "Current"; + static const char* DESCRIPTION = "Description"; + static const char* DICOM_TIMEOUT = "DicomTimeout"; + static const char* NEXT_OPERATIONS = "Next"; + static const char* OPERATION = "Operation"; + static const char* OPERATIONS = "Operations"; + static const char* ORIGINAL_INPUTS = "OriginalInputs"; + static const char* TRAILING_TIMEOUT = "TrailingTimeout"; + static const char* TYPE = "Type"; + static const char* WORK_INPUTS = "WorkInputs"; + + + class SequenceOfOperationsJob::Operation : public boost::noncopyable + { + private: + size_t index_; + std::auto_ptr<IJobOperation> operation_; + std::auto_ptr<JobOperationValues> originalInputs_; + std::auto_ptr<JobOperationValues> workInputs_; + std::list<Operation*> nextOperations_; + size_t currentInput_; + + public: + Operation(size_t index, + IJobOperation* operation) : + index_(index), + operation_(operation), + originalInputs_(new JobOperationValues), + workInputs_(new JobOperationValues), + currentInput_(0) + { + if (operation == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + void AddOriginalInput(const JobOperationValue& value) + { + if (currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + originalInputs_->Append(value.Clone()); + } + } + + const JobOperationValues& GetOriginalInputs() const + { + return *originalInputs_; + } + + void Reset() + { + workInputs_->Clear(); + currentInput_ = 0; + } + + void AddNextOperation(Operation& other, + bool unserializing) + { + if (other.index_ <= index_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!unserializing && + currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + nextOperations_.push_back(&other); + } + } + + bool IsDone() const + { + return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize(); + } + + void Step(IDicomConnectionManager& connectionManager) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const JobOperationValue* input; + + if (currentInput_ < originalInputs_->GetSize()) + { + input = &originalInputs_->GetValue(currentInput_); + } + else + { + input = &workInputs_->GetValue(currentInput_ - originalInputs_->GetSize()); + } + + JobOperationValues outputs; + operation_->Apply(outputs, *input, connectionManager); + + if (!nextOperations_.empty()) + { + std::list<Operation*>::iterator first = nextOperations_.begin(); + outputs.Move(*(*first)->workInputs_); + + std::list<Operation*>::iterator current = first; + ++current; + + while (current != nextOperations_.end()) + { + (*first)->workInputs_->Copy(*(*current)->workInputs_); + ++current; + } + } + + currentInput_ += 1; + } + + void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[CURRENT] = static_cast<unsigned int>(currentInput_); + operation_->Serialize(target[OPERATION]); + originalInputs_->Serialize(target[ORIGINAL_INPUTS]); + workInputs_->Serialize(target[WORK_INPUTS]); + + Json::Value tmp = Json::arrayValue; + for (std::list<Operation*>::const_iterator it = nextOperations_.begin(); + it != nextOperations_.end(); ++it) + { + tmp.append(static_cast<int>((*it)->index_)); + } + + target[NEXT_OPERATIONS] = tmp; + } + + Operation(IJobUnserializer& unserializer, + Json::Value::ArrayIndex index, + const Json::Value& serialized) : + index_(index) + { + if (serialized.type() != Json::objectValue || + !serialized.isMember(OPERATION) || + !serialized.isMember(ORIGINAL_INPUTS) || + !serialized.isMember(WORK_INPUTS)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + currentInput_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + operation_.reset(unserializer.UnserializeOperation(serialized[OPERATION])); + originalInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[ORIGINAL_INPUTS])); + workInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[WORK_INPUTS])); + } + }; + + + SequenceOfOperationsJob::SequenceOfOperationsJob() : + done_(false), + current_(0), + trailingTimeout_(boost::posix_time::milliseconds(1000)) + { + } + + + SequenceOfOperationsJob::~SequenceOfOperationsJob() + { + for (size_t i = 0; i < operations_.size(); i++) + { + if (operations_[i] != NULL) + { + delete operations_[i]; + } + } + } + + + void SequenceOfOperationsJob::SetDescription(const std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description_ = description; + } + + + void SequenceOfOperationsJob::GetDescription(std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description = description_; + } + + + void SequenceOfOperationsJob::Register(IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observers_.push_back(&observer); + } + + + void SequenceOfOperationsJob::Lock::SetTrailingOperationTimeout(unsigned int timeout) + { + that_.trailingTimeout_ = boost::posix_time::milliseconds(timeout); + } + + + void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout) + { + that_.connectionManager_.SetTimeout(timeout); + } + + + size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + size_t index = that_.operations_.size(); + + that_.operations_.push_back(new Operation(index, operation)); + that_.operationAdded_.notify_one(); + + return index; + } + + + void SequenceOfOperationsJob::Lock::AddInput(size_t index, + const JobOperationValue& value) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (index >= that_.operations_.size() || + index < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + that_.operations_[index]->AddOriginalInput(value); + } + } + + + void SequenceOfOperationsJob::Lock::Connect(size_t input, + size_t output) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (input >= output || + input >= that_.operations_.size() || + output >= that_.operations_.size() || + input < that_.current_ || + output < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + Operation& a = *that_.operations_[input]; + Operation& b = *that_.operations_[output]; + a.AddNextOperation(b, false /* not unserializing */); + } + } + + + JobStepResult SequenceOfOperationsJob::ExecuteStep() + { + boost::mutex::scoped_lock lock(mutex_); + + if (current_ == operations_.size()) + { + LOG(INFO) << "Executing the trailing timeout in the sequence of operations"; + operationAdded_.timed_wait(lock, trailingTimeout_); + + if (current_ == operations_.size()) + { + // No operation was added during the trailing timeout: The + // job is over + LOG(INFO) << "The sequence of operations is over"; + done_ = true; + + for (std::list<IObserver*>::iterator it = observers_.begin(); + it != observers_.end(); ++it) + { + (*it)->SignalDone(*this); + } + + connectionManager_.Close(); + return JobStepResult::Success(); + } + else + { + LOG(INFO) << "New operation were added to the sequence of operations"; + } + } + + assert(current_ < operations_.size()); + + while (current_ < operations_.size() && + operations_[current_]->IsDone()) + { + current_++; + } + + if (current_ < operations_.size()) + { + operations_[current_]->Step(connectionManager_); + } + + connectionManager_.CheckTimeout(); + + return JobStepResult::Continue(); + } + + + void SequenceOfOperationsJob::SignalResubmit() + { + boost::mutex::scoped_lock lock(mutex_); + + current_ = 0; + done_ = false; + + for (size_t i = 0; i < operations_.size(); i++) + { + operations_[i]->Reset(); + } + } + + + void SequenceOfOperationsJob::ReleaseResources() + { + boost::mutex::scoped_lock lock(mutex_); + connectionManager_.Close(); + } + + + float SequenceOfOperationsJob::GetProgress() + { + boost::mutex::scoped_lock lock(mutex_); + + return (static_cast<float>(current_) / + static_cast<float>(operations_.size() + 1)); + } + + + void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) + { + boost::mutex::scoped_lock lock(mutex_); + + value["CountOperations"] = static_cast<unsigned int>(operations_.size()); + value["Description"] = description_; + } + + + bool SequenceOfOperationsJob::Serialize(Json::Value& value) + { + boost::mutex::scoped_lock lock(mutex_); + + value = Json::objectValue; + + std::string jobType; + GetJobType(jobType); + value[TYPE] = jobType; + + value[DESCRIPTION] = description_; + value[TRAILING_TIMEOUT] = static_cast<unsigned int>(trailingTimeout_.total_milliseconds()); + value[DICOM_TIMEOUT] = connectionManager_.GetTimeout(); + value[CURRENT] = static_cast<unsigned int>(current_); + + Json::Value tmp = Json::arrayValue; + for (size_t i = 0; i < operations_.size(); i++) + { + Json::Value operation = Json::objectValue; + operations_[i]->Serialize(operation); + tmp.append(operation); + } + + value[OPERATIONS] = tmp; + + return true; + } + + + SequenceOfOperationsJob::SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized) : + done_(false) + { + std::string jobType; + GetJobType(jobType); + + if (SerializationToolbox::ReadString(serialized, TYPE) != jobType || + !serialized.isMember(OPERATIONS) || + serialized[OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION); + trailingTimeout_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT)); + connectionManager_.SetTimeout + (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT)); + current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + + const Json::Value& ops = serialized[OPERATIONS]; + + // Unserialize the individual operations + operations_.reserve(ops.size()); + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + operations_.push_back(new Operation(unserializer, i, ops[i])); + } + + // Connect the next operations + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + if (!ops[i].isMember(NEXT_OPERATIONS) || + ops[i][NEXT_OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& next = ops[i][NEXT_OPERATIONS]; + for (Json::Value::ArrayIndex j = 0; j < next.size(); j++) + { + if (next[j].type() != Json::intValue || + next[j].asInt() < 0 || + next[j].asUInt() >= operations_.size()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + operations_[i]->AddNextOperation(*operations_[next[j].asUInt()], true); + } + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,153 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../IJob.h" +#include "IJobOperation.h" + +#include "../../DicomNetworking/TimeoutDicomConnectionManager.h" + +#include <boost/thread/mutex.hpp> +#include <boost/thread/condition_variable.hpp> + +#include <list> + +namespace Orthanc +{ + class SequenceOfOperationsJob : public IJob + { + public: + class IObserver : public boost::noncopyable + { + public: + virtual ~IObserver() + { + } + + virtual void SignalDone(const SequenceOfOperationsJob& job) = 0; + }; + + private: + class Operation; + + std::string description_; + bool done_; + boost::mutex mutex_; + std::vector<Operation*> operations_; + size_t current_; + boost::condition_variable operationAdded_; + boost::posix_time::time_duration trailingTimeout_; + std::list<IObserver*> observers_; + TimeoutDicomConnectionManager connectionManager_; + + public: + SequenceOfOperationsJob(); + + SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized); + + virtual ~SequenceOfOperationsJob(); + + void SetDescription(const std::string& description); + + void GetDescription(std::string& description); + + void Register(IObserver& observer); + + // This lock allows adding new operations to the end of the job, + // from another thread than the worker thread, after the job has + // been submitted for processing + class Lock : public boost::noncopyable + { + private: + SequenceOfOperationsJob& that_; + boost::mutex::scoped_lock lock_; + + public: + Lock(SequenceOfOperationsJob& that) : + that_(that), + lock_(that.mutex_) + { + } + + bool IsDone() const + { + return that_.done_; + } + + void SetTrailingOperationTimeout(unsigned int timeout); + + void SetDicomAssociationTimeout(unsigned int timeout); + + size_t AddOperation(IJobOperation* operation); + + size_t GetOperationsCount() const + { + return that_.operations_.size(); + } + + void AddInput(size_t index, + const JobOperationValue& value); + + void Connect(size_t input, + size_t output); + }; + + virtual void Start() + { + } + + virtual JobStepResult ExecuteStep(); + + virtual void SignalResubmit(); + + virtual void ReleaseResources(); + + virtual float GetProgress(); + + virtual void GetJobType(std::string& target) + { + target = "SequenceOfOperations"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& value); + + void AwakeTrailingSleep() + { + operationAdded_.notify_one(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/StringOperationValue.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,71 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "JobOperationValue.h" + +#include <string> + +namespace Orthanc +{ + class StringOperationValue : public JobOperationValue + { + private: + std::string content_; + + public: + StringOperationValue(const std::string& content) : + JobOperationValue(JobOperationValue::Type_String), + content_(content) + { + } + + virtual JobOperationValue* Clone() const + { + return new StringOperationValue(content_); + } + + const std::string& GetContent() const + { + return content_; + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "String"; + target["Content"] = content_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfInstancesJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,236 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "SetOfInstancesJob.h" + +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +namespace Orthanc +{ + SetOfInstancesJob::SetOfInstancesJob() : + started_(false), + permissive_(false), + position_(0) + { + } + + + void SetOfInstancesJob::Reserve(size_t size) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instances_.reserve(size); + } + } + + + void SetOfInstancesJob::AddInstance(const std::string& instance) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instances_.push_back(instance); + } + } + + + void SetOfInstancesJob::SetPermissive(bool permissive) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + permissive_ = permissive; + } + } + + + void SetOfInstancesJob::SignalResubmit() + { + if (started_) + { + position_ = 0; + failedInstances_.clear(); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + float SetOfInstancesJob::GetProgress() + { + if (instances_.size() == 0) + { + return 0; + } + else + { + return (static_cast<float>(position_) / + static_cast<float>(instances_.size())); + } + } + + + const std::string& SetOfInstancesJob::GetInstance(size_t index) const + { + if (index > instances_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return instances_[index]; + } + } + + + JobStepResult SetOfInstancesJob::ExecuteStep() + { + if (!started_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (instances_.empty() && + position_ == 0) + { + // No instance to handle, we're done + position_ = 1; + return JobStepResult::Success(); + } + + if (position_ >= instances_.size()) + { + // Already done + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const std::string currentInstance = instances_[position_]; + + bool ok; + + try + { + ok = HandleInstance(currentInstance); + + if (!ok && !permissive_) + { + return JobStepResult::Failure(ErrorCode_InternalError); + } + } + catch (OrthancException& e) + { + if (permissive_) + { + ok = false; + } + else + { + throw; + } + } + + if (!ok) + { + failedInstances_.insert(currentInstance); + } + + position_ += 1; + + if (position_ == instances_.size()) + { + // We're done + return JobStepResult::Success(); + } + else + { + return JobStepResult::Continue(); + } + } + + + void SetOfInstancesJob::GetPublicContent(Json::Value& value) + { + value["Description"] = GetDescription(); + value["InstancesCount"] = static_cast<uint32_t>(instances_.size()); + value["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size()); + } + + + bool SetOfInstancesJob::Serialize(Json::Value& value) + { + value = Json::objectValue; + + std::string type; + GetJobType(type); + value["Type"] = type; + + value["Permissive"] = permissive_; + value["Position"] = static_cast<unsigned int>(position_); + value["Description"] = description_; + + SerializationToolbox::WriteArrayOfStrings(value, instances_, "Instances"); + SerializationToolbox::WriteSetOfStrings(value, failedInstances_, "FailedInstances"); + + return true; + } + + + SetOfInstancesJob::SetOfInstancesJob(const Json::Value& value) : + started_(false), + permissive_(SerializationToolbox::ReadBoolean(value, "Permissive")), + position_(SerializationToolbox::ReadUnsignedInteger(value, "Position")), + description_(SerializationToolbox::ReadString(value, "Description")) + { + SerializationToolbox::ReadArrayOfStrings(instances_, value, "Instances"); + SerializationToolbox::ReadSetOfStrings(failedInstances_, value, "FailedInstances"); + + if (position_ > instances_.size()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfInstancesJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,123 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IJob.h" + +#include <set> + +namespace Orthanc +{ + class SetOfInstancesJob : public IJob + { + private: + bool started_; + std::vector<std::string> instances_; + bool permissive_; + size_t position_; + std::set<std::string> failedInstances_; + std::string description_; + + protected: + virtual bool HandleInstance(const std::string& instance) = 0; + + public: + SetOfInstancesJob(); + + SetOfInstancesJob(const Json::Value& s); // Unserialization + + size_t GetPosition() const + { + return position_; + } + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + void Reserve(size_t size); + + size_t GetInstancesCount() const + { + return instances_.size(); + } + + void AddInstance(const std::string& instance); + + bool IsPermissive() const + { + return permissive_; + } + + void SetPermissive(bool permissive); + + virtual void SignalResubmit(); + + virtual void Start() + { + started_ = true; + } + + virtual float GetProgress(); + + bool IsStarted() const + { + return started_; + } + + const std::string& GetInstance(size_t index) const; + + const std::set<std::string>& GetFailedInstances() const + { + return failedInstances_; + } + + bool IsFailedInstance(const std::string& instance) const + { + return failedInstances_.find(instance) != failedInstances_.end(); + } + + virtual JobStepResult ExecuteStep(); + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& value); + }; +}
--- a/Core/MultiThreading/BagOfTasks.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "../ICommand.h" - -#include <list> -#include <cstddef> - -namespace Orthanc -{ - class BagOfTasks : public boost::noncopyable - { - private: - typedef std::list<ICommand*> Tasks; - - Tasks tasks_; - - public: - ~BagOfTasks() - { - for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it) - { - delete *it; - } - } - - ICommand* Pop() - { - ICommand* task = tasks_.front(); - tasks_.pop_front(); - return task; - } - - void Push(ICommand* task) // Takes ownership - { - if (task != NULL) - { - tasks_.push_back(task); - } - } - - size_t GetSize() const - { - return tasks_.size(); - } - - bool IsEmpty() const - { - return tasks_.empty(); - } - }; -}
--- a/Core/MultiThreading/BagOfTasksProcessor.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,277 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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 "../PrecompiledHeaders.h" -#include "BagOfTasksProcessor.h" - -#include "../Logging.h" -#include "../OrthancException.h" - -#include <stdio.h> - -namespace Orthanc -{ - class BagOfTasksProcessor::Task : public IDynamicObject - { - private: - uint64_t bag_; - std::auto_ptr<ICommand> command_; - - public: - Task(uint64_t bag, - ICommand* command) : - bag_(bag), - command_(command) - { - } - - bool Execute() - { - try - { - return command_->Execute(); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What(); - return false; - } - catch (std::runtime_error& e) - { - LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what(); - return false; - } - catch (...) - { - LOG(ERROR) << "Native exception while processing a bag of tasks"; - return false; - } - } - - uint64_t GetBag() - { - return bag_; - } - }; - - - void BagOfTasksProcessor::SignalProgress(Task& task, - Bag& bag) - { - assert(bag.done_ < bag.size_); - - bag.done_ += 1; - - if (bag.done_ == bag.size_) - { - exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running); - bagFinished_.notify_all(); - } - } - - void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that) - { - while (that->continue_) - { - std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100)); - if (obj.get() != NULL) - { - Task& task = *dynamic_cast<Task*>(obj.get()); - - { - boost::mutex::scoped_lock lock(that->mutex_); - - Bags::iterator bag = that->bags_.find(task.GetBag()); - assert(bag != that->bags_.end()); - assert(bag->second.done_ < bag->second.size_); - - if (bag->second.status_ != BagStatus_Running) - { - // Do not execute this task, as its parent bag of tasks - // has failed or is tagged as canceled - that->SignalProgress(task, bag->second); - continue; - } - } - - bool success = task.Execute(); - - { - boost::mutex::scoped_lock lock(that->mutex_); - - Bags::iterator bag = that->bags_.find(task.GetBag()); - assert(bag != that->bags_.end()); - - if (!success) - { - bag->second.status_ = BagStatus_Failed; - } - - that->SignalProgress(task, bag->second); - } - } - } - } - - - void BagOfTasksProcessor::Cancel(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - Bags::iterator it = bags_.find(bag); - if (it != bags_.end()) - { - it->second.status_ = BagStatus_Canceled; - } - } - - - bool BagOfTasksProcessor::Join(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - while (continue_) - { - ExitStatus::iterator it = exitStatus_.find(bag); - if (it == exitStatus_.end()) // The bag is still running - { - bagFinished_.wait(lock); - } - else - { - bool status = it->second; - exitStatus_.erase(it); - return status; - } - } - - return false; // The processor is stopping - } - - - float BagOfTasksProcessor::GetProgress(int64_t bag) - { - boost::mutex::scoped_lock lock(mutex_); - - Bags::const_iterator it = bags_.find(bag); - if (it == bags_.end()) - { - // The bag of tasks has finished - return 1.0f; - } - else - { - return (static_cast<float>(it->second.done_) / - static_cast<float>(it->second.size_)); - } - } - - - bool BagOfTasksProcessor::Handle::Join() - { - if (hasJoined_) - { - return status_; - } - else - { - status_ = that_.Join(bag_); - hasJoined_ = true; - return status_; - } - } - - - BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : - countBags_(0), - continue_(true) - { - if (countThreads == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - threads_.resize(countThreads); - - for (size_t i = 0; i < threads_.size(); i++) - { - threads_[i] = new boost::thread(Worker, this); - } - } - - - BagOfTasksProcessor::~BagOfTasksProcessor() - { - continue_ = false; - - bagFinished_.notify_all(); // Wakes up all the pending "Join()" - - for (size_t i = 0; i < threads_.size(); i++) - { - if (threads_[i]) - { - if (threads_[i]->joinable()) - { - threads_[i]->join(); - } - - delete threads_[i]; - threads_[i] = NULL; - } - } - } - - - BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks) - { - if (tasks.GetSize() == 0) - { - return new Handle(*this, 0, true); - } - - boost::mutex::scoped_lock lock(mutex_); - - uint64_t id = countBags_; - countBags_ += 1; - - Bag bag(tasks.GetSize()); - bags_[id] = bag; - - while (!tasks.IsEmpty()) - { - queue_.Enqueue(new Task(id, tasks.Pop())); - } - - return new Handle(*this, id, false); - } -}
--- a/Core/MultiThreading/BagOfTasksProcessor.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "BagOfTasks.h" -#include "SharedMessageQueue.h" - -#include <stdint.h> -#include <map> - -namespace Orthanc -{ - class BagOfTasksProcessor : public boost::noncopyable - { - private: - enum BagStatus - { - BagStatus_Running, - BagStatus_Canceled, - BagStatus_Failed - }; - - - struct Bag - { - size_t size_; - size_t done_; - BagStatus status_; - - Bag() : - size_(0), - done_(0), - status_(BagStatus_Failed) - { - } - - explicit Bag(size_t size) : - size_(size), - done_(0), - status_(BagStatus_Running) - { - } - }; - - class Task; - - - typedef std::map<uint64_t, Bag> Bags; - typedef std::map<uint64_t, bool> ExitStatus; - - SharedMessageQueue queue_; - - boost::mutex mutex_; - uint64_t countBags_; - Bags bags_; - std::vector<boost::thread*> threads_; - ExitStatus exitStatus_; - bool continue_; - - boost::condition_variable bagFinished_; - - static void Worker(BagOfTasksProcessor* that); - - void Cancel(int64_t bag); - - bool Join(int64_t bag); - - float GetProgress(int64_t bag); - - void SignalProgress(Task& task, - Bag& bag); - - public: - class Handle : public boost::noncopyable - { - friend class BagOfTasksProcessor; - - private: - BagOfTasksProcessor& that_; - uint64_t bag_; - bool hasJoined_; - bool status_; - - Handle(BagOfTasksProcessor& that, - uint64_t bag, - bool empty) : - that_(that), - bag_(bag), - hasJoined_(empty) - { - } - - public: - ~Handle() - { - Join(); - } - - void Cancel() - { - that_.Cancel(bag_); - } - - bool Join(); - - float GetProgress() - { - return that_.GetProgress(bag_); - } - }; - - - explicit BagOfTasksProcessor(size_t countThreads); - - ~BagOfTasksProcessor(); - - Handle* Submit(BagOfTasks& tasks); - }; -}
--- a/Core/MultiThreading/ILockable.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - class ILockable : public boost::noncopyable - { - friend class Locker; - - protected: - virtual void Lock() = 0; - - virtual void Unlock() = 0; - - public: - virtual ~ILockable() - { - } - }; -}
--- a/Core/MultiThreading/Locker.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "ILockable.h" - -namespace Orthanc -{ - class Locker : public boost::noncopyable - { - private: - ILockable& lockable_; - - public: - Locker(ILockable& lockable) : lockable_(lockable) - { - lockable_.Lock(); - } - - virtual ~Locker() - { - lockable_.Unlock(); - } - }; -}
--- a/Core/MultiThreading/Mutex.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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 "../PrecompiledHeaders.h" -#include "Mutex.h" - -#include "../OrthancException.h" - -#if defined(_WIN32) -#include <windows.h> -#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) -#include <pthread.h> -#else -#error Support your platform here -#endif - -namespace Orthanc -{ -#if defined (_WIN32) - - struct Mutex::PImpl - { - CRITICAL_SECTION criticalSection_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - ::InitializeCriticalSection(&pimpl_->criticalSection_); - } - - Mutex::~Mutex() - { - ::DeleteCriticalSection(&pimpl_->criticalSection_); - delete pimpl_; - } - - void Mutex::Lock() - { - ::EnterCriticalSection(&pimpl_->criticalSection_); - } - - void Mutex::Unlock() - { - ::LeaveCriticalSection(&pimpl_->criticalSection_); - } - - -#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) - - struct Mutex::PImpl - { - pthread_mutex_t mutex_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - - if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0) - { - delete pimpl_; - throw OrthancException(ErrorCode_InternalError); - } - } - - Mutex::~Mutex() - { - pthread_mutex_destroy(&pimpl_->mutex_); - delete pimpl_; - } - - void Mutex::Lock() - { - if (pthread_mutex_lock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - - void Mutex::Unlock() - { - if (pthread_mutex_unlock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - -#else -#error Support your plateform here -#endif -}
--- a/Core/MultiThreading/Mutex.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "ILockable.h" - -namespace Orthanc -{ - class Mutex : public ILockable - { - private: - struct PImpl; - - PImpl *pimpl_; - - protected: - virtual void Lock(); - - virtual void Unlock(); - - public: - Mutex(); - - ~Mutex(); - }; -}
--- a/Core/MultiThreading/ReaderWriterLock.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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 "../PrecompiledHeaders.h" -#include "ReaderWriterLock.h" - -#include <boost/thread/shared_mutex.hpp> - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation - // modules. - - class ReaderLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock_shared(); - } - - virtual void Unlock() - { - lock_.unlock_shared(); - } - - public: - explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - }; - - - class WriterLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock(); - } - - virtual void Unlock() - { - lock_.unlock(); - } - - public: - explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - }; - } - - struct ReaderWriterLock::PImpl - { - boost::shared_mutex lock_; - ReaderLockable reader_; - WriterLockable writer_; - - PImpl() : reader_(lock_), writer_(lock_) - { - } - }; - - - ReaderWriterLock::ReaderWriterLock() - { - pimpl_ = new PImpl; - } - - - ReaderWriterLock::~ReaderWriterLock() - { - delete pimpl_; - } - - - ILockable& ReaderWriterLock::ForReader() - { - return pimpl_->reader_; - } - - - ILockable& ReaderWriterLock::ForWriter() - { - return pimpl_->writer_; - } -}
--- a/Core/MultiThreading/ReaderWriterLock.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "ILockable.h" - -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - class ReaderWriterLock : public boost::noncopyable - { - private: - struct PImpl; - - PImpl *pimpl_; - - public: - ReaderWriterLock(); - - virtual ~ReaderWriterLock(); - - ILockable& ForReader(); - - ILockable& ForWriter(); - }; -}
--- a/Core/MultiThreading/Semaphore.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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 "../PrecompiledHeaders.h" -#include "Semaphore.h" - -#include "../OrthancException.h" - - -namespace Orthanc -{ - Semaphore::Semaphore(unsigned int count) : count_(count) - { - } - - void Semaphore::Release() - { - boost::mutex::scoped_lock lock(mutex_); - - count_++; - condition_.notify_one(); - } - - void Semaphore::Acquire() - { - boost::mutex::scoped_lock lock(mutex_); - - while (count_ == 0) - { - condition_.wait(lock); - } - - count_++; - } -}
--- a/Core/MultiThreading/Semaphore.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include <boost/noncopyable.hpp> -#include <boost/thread.hpp> - -namespace Orthanc -{ - class Semaphore : public boost::noncopyable - { - private: - unsigned int count_; - boost::mutex mutex_; - boost::condition_variable condition_; - - public: - explicit Semaphore(unsigned int count); - - void Release(); - - void Acquire(); - - class Locker : public boost::noncopyable - { - private: - Semaphore& that_; - - public: - explicit Locker(Semaphore& that) : - that_(that) - { - that_.Acquire(); - } - - ~Locker() - { - that_.Release(); - } - }; - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SerializationToolbox.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,262 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "PrecompiledHeaders.h" +#include "SerializationToolbox.h" + +#include "OrthancException.h" + +namespace Orthanc +{ + namespace SerializationToolbox + { + std::string ReadString(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asString(); + } + } + + + int ReadInteger(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + (value[field.c_str()].type() != Json::intValue && + value[field.c_str()].type() != Json::uintValue)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asInt(); + } + } + + + unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field) + { + int tmp = ReadInteger(value, field); + + if (tmp < 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return static_cast<unsigned int>(tmp); + } + } + + + bool ReadBoolean(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asBool(); + } + } + + + void ReadArrayOfStrings(std::vector<std::string>& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& arr = value[field.c_str()]; + + target.resize(arr.size()); + + for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++) + { + if (arr[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target[i] = arr[i].asString(); + } + } + } + + + void ReadListOfStrings(std::list<std::string>& target, + const Json::Value& value, + const std::string& field) + { + std::vector<std::string> tmp; + ReadArrayOfStrings(tmp, value, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.push_back(tmp[i]); + } + } + + + void ReadSetOfStrings(std::set<std::string>& target, + const Json::Value& value, + const std::string& field) + { + std::vector<std::string> tmp; + ReadArrayOfStrings(tmp, value, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.insert(tmp[i]); + } + } + + + void ReadSetOfTags(std::set<DicomTag>& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& arr = value[field.c_str()]; + + target.clear(); + + for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++) + { + DicomTag tag(0, 0); + + if (arr[i].type() != Json::stringValue || + !DicomTag::ParseHexadecimal(tag, arr[i].asCString())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target.insert(tag); + } + } + } + + + void WriteArrayOfStrings(Json::Value& target, + const std::vector<std::string>& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + for (size_t i = 0; i < values.size(); i++) + { + value.append(values[i]); + } + } + + + void WriteSetOfStrings(Json::Value& target, + const std::set<std::string>& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::set<std::string>::const_iterator it = values.begin(); + it != values.end(); ++it) + { + value.append(*it); + } + } + + + void WriteSetOfTags(Json::Value& target, + const std::set<DicomTag>& tags, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::set<DicomTag>::const_iterator it = tags.begin(); + it != tags.end(); ++it) + { + value.append(it->Format()); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SerializationToolbox.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "DicomFormat/DicomTag.h" + +#include <json/value.h> +#include <list> + +namespace Orthanc +{ + namespace SerializationToolbox + { + std::string ReadString(const Json::Value& value, + const std::string& field); + + int ReadInteger(const Json::Value& value, + const std::string& field); + + unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field); + + bool ReadBoolean(const Json::Value& value, + const std::string& field); + + void ReadArrayOfStrings(std::vector<std::string>& target, + const Json::Value& value, + const std::string& field); + + void ReadListOfStrings(std::list<std::string>& target, + const Json::Value& value, + const std::string& field); + + void ReadSetOfStrings(std::set<std::string>& target, + const Json::Value& value, + const std::string& field); + + void ReadSetOfTags(std::set<DicomTag>& target, + const Json::Value& value, + const std::string& field); + + void WriteArrayOfStrings(Json::Value& target, + const std::vector<std::string>& values, + const std::string& field); + + void WriteSetOfStrings(Json::Value& target, + const std::set<std::string>& values, + const std::string& field); + + void WriteSetOfTags(Json::Value& target, + const std::set<DicomTag>& tags, + const std::string& field); + } +}
--- a/Core/WebServiceParameters.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/WebServiceParameters.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,8 +34,9 @@ #include "PrecompiledHeaders.h" #include "WebServiceParameters.h" -#include "../Core/Logging.h" -#include "../Core/OrthancException.h" +#include "Logging.h" +#include "OrthancException.h" +#include "SerializationToolbox.h" #if ORTHANC_SANDBOXED == 0 # include "../Core/SystemToolbox.h" @@ -129,6 +130,11 @@ SetUsername(""); SetPassword(""); } + else if (peer.size() == 2) + { + LOG(ERROR) << "The HTTP password is not provided"; + throw OrthancException(ErrorCode_BadFileFormat); + } else if (peer.size() == 3) { SetUsername(peer.get(1u, "").asString()); @@ -177,12 +183,25 @@ SetUsername(GetStringMember(peer, "Username", "")); SetPassword(GetStringMember(peer, "Password", "")); + if (!username_.empty() && + !peer.isMember("Password")) + { + LOG(ERROR) << "The HTTP password is not provided"; + throw OrthancException(ErrorCode_BadFileFormat); + } + #if ORTHANC_SANDBOXED == 0 if (peer.isMember("CertificateFile")) { SetClientCertificate(GetStringMember(peer, "CertificateFile", ""), GetStringMember(peer, "CertificateKeyFile", ""), GetStringMember(peer, "CertificateKeyPassword", "")); + + if (!peer.isMember("CertificateKeyPassword")) + { + LOG(ERROR) << "The password for the HTTPS certificate is not provided"; + throw OrthancException(ErrorCode_BadFileFormat); + } } #endif @@ -228,7 +247,8 @@ } - void WebServiceParameters::ToJson(Json::Value& value) const + void WebServiceParameters::ToJson(Json::Value& value, + bool includePasswords) const { if (advancedFormat_) { @@ -239,7 +259,11 @@ !password_.empty()) { value["Username"] = username_; - value["Password"] = password_; + + if (includePasswords) + { + value["Password"] = password_; + } } if (!certificateFile_.empty()) @@ -252,7 +276,8 @@ value["CertificateKeyFile"] = certificateKeyFile_; } - if (!certificateKeyPassword_.empty()) + if (!certificateKeyPassword_.empty() && + includePasswords) { value["CertificateKeyPassword"] = certificateKeyPassword_; } @@ -266,8 +291,48 @@ !password_.empty()) { value.append(username_); - value.append(password_); + + if (includePasswords) + { + value.append(password_); + } } } } + + + void WebServiceParameters::Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["URL"] = url_; + target["Username"] = username_; + target["Password"] = password_; + target["CertificateFile"] = certificateFile_; + target["CertificateKeyFile"] = certificateKeyFile_; + target["CertificateKeyPassword"] = certificateKeyPassword_; + target["PKCS11"] = pkcs11Enabled_; + target["AdvancedFormat"] = advancedFormat_; + } + + + WebServiceParameters::WebServiceParameters(const Json::Value& serialized) : + advancedFormat_(true) + { + url_ = SerializationToolbox::ReadString(serialized, "URL"); + username_ = SerializationToolbox::ReadString(serialized, "Username"); + password_ = SerializationToolbox::ReadString(serialized, "Password"); + + std::string a, b, c; + a = SerializationToolbox::ReadString(serialized, "CertificateFile"); + b = SerializationToolbox::ReadString(serialized, "CertificateKeyFile"); + c = SerializationToolbox::ReadString(serialized, "CertificateKeyPassword"); + + if (!a.empty()) + { + SetClientCertificate(a, b, c); + } + + pkcs11Enabled_ = SerializationToolbox::ReadBoolean(serialized, "PKCS11"); + advancedFormat_ = SerializationToolbox::ReadBoolean(serialized, "AdvancedFormat"); + } }
--- a/Core/WebServiceParameters.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Core/WebServiceParameters.h Mon Jun 11 16:30:13 2018 +0200 @@ -61,6 +61,8 @@ public: WebServiceParameters(); + WebServiceParameters(const Json::Value& serialized); + const std::string& GetUrl() const { return url_; @@ -126,6 +128,9 @@ void FromJson(const Json::Value& peer); - void ToJson(Json::Value& value) const; + void ToJson(Json::Value& value, + bool includePasswords) const; + + void Serialize(Json::Value& target) const; }; }
--- a/NEWS Thu Jun 07 17:15:55 2018 +0200 +++ b/NEWS Mon Jun 11 16:30:13 2018 +0200 @@ -1,6 +1,15 @@ Pending changes in the mainline =============================== +General +------- + +* New advanced job engine +* New configuration options: + - "ConcurrentJobs": Max number of jobs that are simultanously running + - "SynchronousCMove": Whether to run DICOM C-Move operations synchronously + - "JobsHistorySize": Max number of completed jobs that are kept in memory + Orthanc Explorer ---------------- @@ -9,6 +18,7 @@ REST API -------- +* "/jobs/..." to manage the jobs from the REST API * New option "?short" to list DICOM tags using their hexadecimal ID in: - "/instances/.../tags?short" - "/instances/.../header?short" @@ -27,6 +37,7 @@ ----------- * Fix generation of DICOMDIR if PatientID is empty +* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua Version 1.3.2 (2018-04-18)
--- a/OrthancExplorer/explorer.html Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancExplorer/explorer.html Mon Jun 11 16:30:13 2018 +0200 @@ -38,12 +38,13 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Find a patient</h1> <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> + <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a> <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a> - <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a> </div> <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -56,12 +57,13 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Find a study</h1> <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> + <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a> <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a> - <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a> </div> <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -109,6 +111,7 @@ <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -169,6 +172,7 @@ <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -223,6 +227,7 @@ <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -279,6 +284,7 @@ <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> <a href="#upload" data-icon="gear" data-role="button">Upload</a> <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> </div> </div> <div data-role="content"> @@ -468,6 +474,48 @@ </div> </div> + + <div data-role="page" id="jobs" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>Jobs</h1> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> + <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a> + <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a> + </div> + </div> + <div data-role="content"> + <ul id="all-jobs" data-role="listview" data-inset="true" data-filter="true"> + </ul> + </div> + </div> + + <div data-role="page" id="job" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>Job</h1> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> + <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a> + <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a> + </div> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a> + </div> + </div> + <div data-role="content"> + <ul data-role="listview" data-inset="true" data-filter="true" id="job-info"> + </ul> + + <fieldset class="ui-grid-b"> + <div class="ui-block-a"></div> + <div class="ui-block-b"> + <button id="job-cancel" data-theme="b">Cancel job</button> + <button id="job-resubmit" data-theme="b">Resubmit job</button> + <button id="job-pause" data-theme="b">Pause job</button> + <button id="job-resume" data-theme="b">Resume job</button> + </div> + <div class="ui-block-c"></div> + </fieldset> + </div> + </div> <div id="peer-store" style="display:none;" class="ui-body-c"> <p align="center"><b>Sending to Orthanc peer...</b></p>
--- a/OrthancExplorer/explorer.js Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancExplorer/explorer.js Mon Jun 11 16:30:13 2018 +0200 @@ -1156,3 +1156,209 @@ } }); }); + + + +function ParseJobTime(s) +{ + var t = (s.substr(0, 4) + '-' + + s.substr(4, 2) + '-' + + s.substr(6, 5) + ':' + + s.substr(11, 2) + ':' + + s.substr(13)); + var utc = new Date(t); + + // Convert from UTC to local time + return new Date(utc.getTime() - utc.getTimezoneOffset() * 60000); +} + + +function AddJobField(target, description, field) +{ + if (!(typeof field === 'undefined')) { + target.append($('<p>') + .text(description) + .append($('<strong>').text(field))); + } +} + + +function AddJobDateField(target, description, field) +{ + if (!(typeof field === 'undefined')) { + target.append($('<p>') + .text(description) + .append($('<strong>').text(ParseJobTime(field)))); + } +} + + +$('#jobs').live('pagebeforeshow', function() { + $.ajax({ + url: '../jobs?expand', + dataType: 'json', + async: false, + cache: false, + success: function(jobs) { + var target = $('#all-jobs'); + $('li', target).remove(); + + var running = $('<li>') + .attr('data-role', 'list-divider') + .text('Currently running'); + + var pending = $('<li>') + .attr('data-role', 'list-divider') + .text('Pending jobs'); + + var inactive = $('<li>') + .attr('data-role', 'list-divider') + .text('Inactive jobs'); + + target.append(running); + target.append(pending); + target.append(inactive); + + jobs.map(function(job) { + var li = $('<li>'); + var item = $('<a>'); + li.append(item); + item.attr('href', '#job?uuid=' + job.ID); + item.append($('<h1>').text(job.Type)); + item.append($('<span>').addClass('ui-li-count').text(job.State)); + AddJobField(item, 'ID: ', job.ID); + AddJobField(item, 'Local AET: ', job.Content.LocalAet); + AddJobField(item, 'Remote AET: ', job.Content.RemoteAet); + AddJobDateField(item, 'Creation time: ', job.CreationTime); + AddJobDateField(item, 'Completion time: ', job.CompletionTime); + AddJobDateField(item, 'ETA: ', job.EstimatedTimeOfArrival); + + if (job.State == 'Running' || + job.State == 'Pending' || + job.State == 'Paused') { + AddJobField(item, 'Priority: ', job.Priority); + AddJobField(item, 'Progress: ', job.Progress); + } + + if (job.State == 'Running') { + li.insertAfter(running); + } else if (job.State == 'Pending' || + job.State == 'Paused') { + li.insertAfter(pending); + } else { + li.insertAfter(inactive); + } + }); + + target.listview('refresh'); + } + }); +}); + + +$('#job').live('pagebeforeshow', function() { + if ($.mobile.pageData) { + var pageData = DeepCopy($.mobile.pageData); + + $.ajax({ + url: '../jobs/' + pageData.uuid, + dataType: 'json', + async: false, + cache: false, + success: function(job) { + var target = $('#job-info'); + $('li', target).remove(); + + target.append($('<li>') + .attr('data-role', 'list-divider') + .text('General information about the job')); + + var block = $('<li>'); + for (var i in job) { + if (i == 'CreationTime' || + i == 'CompletionTime' || + i == 'EstimatedTimeOfArrival') { + AddJobDateField(block, i + ': ', job[i]); + } else if (i != 'InternalContent' && + i != 'Content' && + i != 'Timestamp') { + AddJobField(block, i + ': ', job[i]); + } + } + + target.append(block); + + target.append($('<li>') + .attr('data-role', 'list-divider') + .text('Detailed information')); + + var block = $('<li>'); + + for (var item in job.Content) { + var value = job.Content[item]; + if (typeof value !== 'string') { + value = JSON.stringify(value); + } + + AddJobField(block, item + ': ', value); + } + + target.append(block); + + target.listview('refresh'); + + $('#job-cancel').closest('.ui-btn').hide(); + $('#job-retry').closest('.ui-btn').hide(); + $('#job-resubmit').closest('.ui-btn').hide(); + $('#job-pause').closest('.ui-btn').hide(); + $('#job-resume').closest('.ui-btn').hide(); + + if (job.State == 'Running' || + job.State == 'Pending' || + job.State == 'Retry') { + $('#job-cancel').closest('.ui-btn').show(); + $('#job-pause').closest('.ui-btn').show(); + } + else if (job.State == 'Success') { + } + else if (job.State == 'Failure') { + $('#job-resubmit').closest('.ui-btn').show(); + } + else if (job.State == 'Paused') { + $('#job-resume').closest('.ui-btn').show(); + } + } + }); + } +}); + + + +function TriggerJobAction(action) +{ + $.ajax({ + url: '../jobs/' + $.mobile.pageData.uuid + '/' + action, + type: 'POST', + async: false, + cache: false, + complete: function(s) { + window.location.reload(); + } + }); +} + +$('#job-cancel').live('click', function() { + TriggerJobAction('cancel'); +}); + +$('#job-resubmit').live('click', function() { + TriggerJobAction('resubmit'); +}); + +$('#job-pause').live('click', function() { + TriggerJobAction('pause'); +}); + +$('#job-resume').live('click', function() { + TriggerJobAction('resume'); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceOrigin.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,195 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "DicomInstanceOrigin.h" + +#include "../Core/OrthancException.h" +#include "../Core/SerializationToolbox.h" + + +namespace Orthanc +{ + void DicomInstanceOrigin::Format(Json::Value& result) const + { + result = Json::objectValue; + result["RequestOrigin"] = EnumerationToString(origin_); + + switch (origin_) + { + case RequestOrigin_Unknown: + { + // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()", + // "SetLuaOrigin()" or "SetPluginsOrigin()" was called! + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + case RequestOrigin_DicomProtocol: + { + result["RemoteIp"] = remoteIp_; + result["RemoteAet"] = dicomRemoteAet_; + result["CalledAet"] = dicomCalledAet_; + break; + } + + case RequestOrigin_RestApi: + { + result["RemoteIp"] = remoteIp_; + result["Username"] = httpUsername_; + break; + } + + case RequestOrigin_Lua: + case RequestOrigin_Plugins: + { + // No additional information available for these kinds of requests + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + DicomInstanceOrigin DicomInstanceOrigin::FromDicomProtocol(const char* remoteIp, + const char* remoteAet, + const char* calledAet) + { + DicomInstanceOrigin result(RequestOrigin_DicomProtocol); + result.remoteIp_ = remoteIp; + result.dicomRemoteAet_ = remoteAet; + result.dicomCalledAet_ = calledAet; + return result; + } + + DicomInstanceOrigin DicomInstanceOrigin::FromRest(const RestApiCall& call) + { + DicomInstanceOrigin result(call.GetRequestOrigin()); + + if (result.origin_ == RequestOrigin_RestApi) + { + result.remoteIp_ = call.GetRemoteIp(); + result.httpUsername_ = call.GetUsername(); + } + + return result; + } + + DicomInstanceOrigin DicomInstanceOrigin::FromHttp(const char* remoteIp, + const char* username) + { + DicomInstanceOrigin result(RequestOrigin_RestApi); + result.remoteIp_ = remoteIp; + result.httpUsername_ = username; + return result; + } + + const char* DicomInstanceOrigin::GetRemoteAetC() const + { + if (origin_ == RequestOrigin_DicomProtocol) + { + return dicomRemoteAet_.c_str(); + } + else + { + return ""; + } + } + + const std::string& DicomInstanceOrigin::GetRemoteIp() const + { + if (origin_ == RequestOrigin_DicomProtocol || + origin_ == RequestOrigin_RestApi) + { + return remoteIp_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + const std::string& DicomInstanceOrigin::GetCalledAet() const + { + if (origin_ == RequestOrigin_DicomProtocol) + { + return dicomCalledAet_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + const std::string& DicomInstanceOrigin::GetHttpUsername() const + { + if (origin_ == RequestOrigin_RestApi) + { + return httpUsername_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + + static const char* ORIGIN = "Origin"; + static const char* REMOTE_IP = "RemoteIP"; + static const char* DICOM_REMOTE_AET = "RemoteAET"; + static const char* DICOM_CALLED_AET = "CalledAET"; + static const char* HTTP_USERNAME = "Username"; + + + DicomInstanceOrigin::DicomInstanceOrigin(const Json::Value& serialized) + { + origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, ORIGIN)); + remoteIp_ = SerializationToolbox::ReadString(serialized, REMOTE_IP); + dicomRemoteAet_ = SerializationToolbox::ReadString(serialized, DICOM_REMOTE_AET); + dicomCalledAet_ = SerializationToolbox::ReadString(serialized, DICOM_CALLED_AET); + httpUsername_ = SerializationToolbox::ReadString(serialized, HTTP_USERNAME); + } + + + void DicomInstanceOrigin::Serialize(Json::Value& result) const + { + result = Json::objectValue; + result[ORIGIN] = EnumerationToString(origin_); + result[REMOTE_IP] = remoteIp_; + result[DICOM_REMOTE_AET] = dicomRemoteAet_; + result[DICOM_CALLED_AET] = dicomCalledAet_; + result[HTTP_USERNAME] = httpUsername_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceOrigin.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,98 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../Core/RestApi/RestApiCall.h" + +namespace Orthanc +{ + class DicomInstanceOrigin + { + private: + RequestOrigin origin_; + std::string remoteIp_; + std::string dicomRemoteAet_; + std::string dicomCalledAet_; + std::string httpUsername_; + + DicomInstanceOrigin(RequestOrigin origin) : + origin_(origin) + { + } + + public: + DicomInstanceOrigin() : + origin_(RequestOrigin_Unknown) + { + } + + DicomInstanceOrigin(const Json::Value& serialized); + + static DicomInstanceOrigin FromDicomProtocol(const char* remoteIp, + const char* remoteAet, + const char* calledAet); + + static DicomInstanceOrigin FromRest(const RestApiCall& call); + + static DicomInstanceOrigin FromHttp(const char* remoteIp, + const char* username); + + static DicomInstanceOrigin FromLua() + { + return DicomInstanceOrigin(RequestOrigin_Lua); + } + + static DicomInstanceOrigin FromPlugins() + { + return DicomInstanceOrigin(RequestOrigin_Plugins); + } + + RequestOrigin GetRequestOrigin() const + { + return origin_; + } + + const char* GetRemoteAetC() const; + + const std::string& GetRemoteIp() const; + + const std::string& GetCalledAet() const; + + const std::string& GetHttpUsername() const; + + void Format(Json::Value& result) const; + + void Serialize(Json::Value& result) const; + }; +}
--- a/OrthancServer/DicomInstanceToStore.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -181,101 +181,6 @@ } - - void DicomInstanceToStore::GetOriginInformation(Json::Value& result) const - { - result = Json::objectValue; - result["RequestOrigin"] = EnumerationToString(origin_); - - switch (origin_) - { - case RequestOrigin_Unknown: - { - // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()", - // "SetLuaOrigin()" or "SetPluginsOrigin()" was called! - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - case RequestOrigin_DicomProtocol: - { - result["RemoteIp"] = remoteIp_; - result["RemoteAet"] = dicomRemoteAet_; - result["CalledAet"] = dicomCalledAet_; - break; - } - - case RequestOrigin_RestApi: - { - result["RemoteIp"] = remoteIp_; - result["Username"] = httpUsername_; - break; - } - - case RequestOrigin_Lua: - case RequestOrigin_Plugins: - { - // No additional information available for these kinds of requests - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - void DicomInstanceToStore::SetDicomProtocolOrigin(const char* remoteIp, - const char* remoteAet, - const char* calledAet) - { - origin_ = RequestOrigin_DicomProtocol; - remoteIp_ = remoteIp; - dicomRemoteAet_ = remoteAet; - dicomCalledAet_ = calledAet; - } - - void DicomInstanceToStore::SetRestOrigin(const RestApiCall& call) - { - origin_ = call.GetRequestOrigin(); - - if (origin_ == RequestOrigin_RestApi) - { - remoteIp_ = call.GetRemoteIp(); - httpUsername_ = call.GetUsername(); - } - } - - void DicomInstanceToStore::SetHttpOrigin(const char* remoteIp, - const char* username) - { - origin_ = RequestOrigin_RestApi; - remoteIp_ = remoteIp; - httpUsername_ = username; - } - - void DicomInstanceToStore::SetLuaOrigin() - { - origin_ = RequestOrigin_Lua; - } - - void DicomInstanceToStore::SetPluginsOrigin() - { - origin_ = RequestOrigin_Plugins; - } - - const char* DicomInstanceToStore::GetRemoteAet() const - { - if (origin_ == RequestOrigin_DicomProtocol) - { - return dicomRemoteAet_.c_str(); - } - else - { - return ""; - } - } - - bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) { ComputeMissingInformation();
--- a/OrthancServer/DicomInstanceToStore.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.h Mon Jun 11 16:30:13 2018 +0200 @@ -34,9 +34,9 @@ #pragma once #include "../Core/DicomParsing/ParsedDicomFile.h" +#include "../Core/OrthancException.h" +#include "DicomInstanceOrigin.h" #include "ServerIndex.h" -#include "../Core/OrthancException.h" -#include "../Core/RestApi/RestApiCall.h" namespace Orthanc { @@ -139,46 +139,26 @@ } }; - - SmartContainer<std::string> buffer_; + DicomInstanceOrigin origin_; + SmartContainer<std::string> buffer_; SmartContainer<ParsedDicomFile> parsed_; - SmartContainer<DicomMap> summary_; - SmartContainer<Json::Value> json_; - - RequestOrigin origin_; - std::string remoteIp_; - std::string dicomRemoteAet_; - std::string dicomCalledAet_; - std::string httpUsername_; - ServerIndex::MetadataMap metadata_; + SmartContainer<DicomMap> summary_; + SmartContainer<Json::Value> json_; + ServerIndex::MetadataMap metadata_; void ComputeMissingInformation(); public: - DicomInstanceToStore() : origin_(RequestOrigin_Unknown) + void SetOrigin(const DicomInstanceOrigin& origin) { + origin_ = origin; } - - void SetDicomProtocolOrigin(const char* remoteIp, - const char* remoteAet, - const char* calledAet); - - void SetRestOrigin(const RestApiCall& call); - - void SetHttpOrigin(const char* remoteIp, - const char* username); - - void SetLuaOrigin(); - - void SetPluginsOrigin(); - - RequestOrigin GetRequestOrigin() const + + const DicomInstanceOrigin& GetOrigin() const { return origin_; } - - const char* GetRemoteAet() const; - + void SetBuffer(const std::string& dicom) { buffer_.SetConstReference(dicom); @@ -221,8 +201,6 @@ const Json::Value& GetJson(); - void GetOriginInformation(Json::Value& result) const; - bool LookupTransferSyntax(std::string& result); }; }
--- a/OrthancServer/IServerListener.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/IServerListener.h Mon Jun 11 16:30:13 2018 +0200 @@ -40,7 +40,7 @@ namespace Orthanc { - class IServerListener + class IServerListener : public boost::noncopyable { public: virtual ~IServerListener()
--- a/OrthancServer/LuaScripting.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/LuaScripting.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,24 +34,159 @@ #include "PrecompiledHeadersServer.h" #include "LuaScripting.h" +#include "OrthancInitialization.h" +#include "OrthancRestApi/OrthancRestApi.h" #include "ServerContext.h" -#include "OrthancInitialization.h" -#include "../Core/Lua/LuaFunctionCall.h" + #include "../Core/HttpServer/StringHttpOutput.h" #include "../Core/Logging.h" - -#include "Scheduler/DeleteInstanceCommand.h" -#include "Scheduler/StoreScuCommand.h" -#include "Scheduler/StorePeerCommand.h" -#include "Scheduler/ModifyInstanceCommand.h" -#include "Scheduler/CallSystemCommand.h" -#include "OrthancRestApi/OrthancRestApi.h" +#include "../Core/Lua/LuaFunctionCall.h" #include <EmbeddedResources.h> namespace Orthanc { + class LuaScripting::IEvent : public IDynamicObject + { + public: + virtual void Apply(LuaScripting& lock) = 0; + }; + + + class LuaScripting::OnStoredInstanceEvent : public LuaScripting::IEvent + { + private: + std::string instanceId_; + Json::Value simplifiedTags_; + Json::Value metadata_; + Json::Value origin_; + + public: + OnStoredInstanceEvent(const std::string& instanceId, + const Json::Value& simplifiedTags, + const Json::Value& metadata, + const DicomInstanceToStore& instance) : + instanceId_(instanceId), + simplifiedTags_(simplifiedTags), + metadata_(metadata) + { + instance.GetOrigin().Format(origin_); + } + + virtual void Apply(LuaScripting& that) + { + static const char* NAME = "OnStoredInstance"; + + LuaScripting::Lock lock(that); + + if (lock.GetLua().IsExistingFunction(NAME)) + { + that.InitializeJob(); + + LuaFunctionCall call(lock.GetLua(), NAME); + call.PushString(instanceId_); + call.PushJson(simplifiedTags_); + call.PushJson(metadata_); + call.PushJson(origin_); + call.Execute(); + + that.SubmitJob(); + } + } + }; + + + class LuaScripting::ExecuteEvent : public LuaScripting::IEvent + { + private: + std::string command_; + + public: + ExecuteEvent(const std::string& command) : + command_(command) + { + } + + virtual void Apply(LuaScripting& that) + { + LuaScripting::Lock lock(that); + + if (lock.GetLua().IsExistingFunction(command_.c_str())) + { + LuaFunctionCall call(lock.GetLua(), command_.c_str()); + call.Execute(); + } + } + }; + + + class LuaScripting::StableResourceEvent : public LuaScripting::IEvent + { + private: + ServerIndexChange change_; + + public: + StableResourceEvent(const ServerIndexChange& change) : + change_(change) + { + } + + virtual void Apply(LuaScripting& that) + { + const char* name; + + switch (change_.GetChangeType()) + { + case ChangeType_StablePatient: + name = "OnStablePatient"; + break; + + case ChangeType_StableStudy: + name = "OnStableStudy"; + break; + + case ChangeType_StableSeries: + name = "OnStableSeries"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + { + // Avoid unnecessary calls to the database if there's no Lua callback + LuaScripting::Lock lock(that); + + if (!lock.GetLua().IsExistingFunction(name)) + { + return; + } + } + + Json::Value tags, metadata; + if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()) && + that.context_.GetIndex().GetMetadata(metadata, change_.GetPublicId())) + { + LuaScripting::Lock lock(that); + + if (lock.GetLua().IsExistingFunction(name)) + { + that.InitializeJob(); + + LuaFunctionCall call(lock.GetLua(), name); + call.PushString(change_.GetPublicId()); + call.PushJson(tags["MainDicomTags"]); + call.PushJson(metadata); + call.Execute(); + + that.SubmitJob(); + } + } + } + }; + + ServerContext* LuaScripting::GetServerContext(lua_State *state) { const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext"); @@ -211,7 +346,7 @@ } LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri; - lua_pushnil(state); + lua_pushnil(state); return 1; } @@ -229,13 +364,14 @@ } - IServerCommand* LuaScripting::ParseOperation(const std::string& operation, - const Json::Value& parameters) + size_t LuaScripting::ParseOperation(LuaJobManager::Lock& lock, + const std::string& operation, + const Json::Value& parameters) { if (operation == "delete") { LOG(INFO) << "Lua script to delete resource " << parameters["Resource"].asString(); - return new DeleteInstanceCommand(context_); + return lock.AddDeleteResourceOperation(context_); } if (operation == "store-scu") @@ -250,36 +386,29 @@ localAet = context_.GetDefaultLocalApplicationEntityTitle(); } - std::string modality = parameters["Modality"].asString(); - LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString() - << " to modality " << modality << " using Store-SCU"; + std::string name = parameters["Modality"].asString(); + RemoteModalityParameters modality = Configuration::GetModalityUsingSymbolicName(name); // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()" - return new StoreScuCommand(context_, localAet, - Configuration::GetModalityUsingSymbolicName(modality), true); + return lock.AddStoreScuOperation(localAet, modality); } if (operation == "store-peer") { - std::string peer = parameters["Peer"].asString(); - LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString() - << " to peer " << peer << " using HTTP"; + std::string name = parameters["Peer"].asString(); - WebServiceParameters parameters; - Configuration::GetOrthancPeer(parameters, peer); - return new StorePeerCommand(context_, parameters, true); + WebServiceParameters peer; + Configuration::GetOrthancPeer(peer, name); + + return lock.AddStorePeerOperation(peer); } if (operation == "modify") { - LOG(INFO) << "Lua script to modify resource " << parameters["Resource"].asString(); std::auto_ptr<DicomModification> modification(new DicomModification); modification->ParseModifyRequest(parameters); - std::auto_ptr<ModifyInstanceCommand> command - (new ModifyInstanceCommand(context_, RequestOrigin_Lua, modification.release())); - - return command.release(); + return lock.AddModifyInstanceOperation(context_, modification.release()); } if (operation == "call-system") @@ -320,7 +449,10 @@ } } - return new CallSystemCommand(context_, parameters["Command"].asString(), args); + std::string command = parameters["Command"].asString(); + std::vector<std::string> postArgs; + + return lock.AddSystemCallOperation(command, args, postArgs); } throw OrthancException(ErrorCode_ParameterOutOfRange); @@ -333,7 +465,7 @@ } - void LuaScripting::SubmitJob(const std::string& description) + void LuaScripting::SubmitJob() { Json::Value operations; LuaFunctionCall call2(lua_, "_AccessJob"); @@ -344,8 +476,10 @@ throw OrthancException(ErrorCode_InternalError); } - ServerJob job; - ServerCommandInstance* previousCommand = NULL; + LuaJobManager::Lock lock(jobManager_, context_.GetJobsEngine()); + + bool isFirst = true; + size_t previous; for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i) { @@ -356,34 +490,33 @@ } const Json::Value& parameters = operations[i]; - std::string operation = parameters["Operation"].asString(); - - ServerCommandInstance& command = job.AddCommand(ParseOperation(operation, operations[i])); - if (!parameters.isMember("Resource")) { throw OrthancException(ErrorCode_InternalError); } + std::string operation = parameters["Operation"].asString(); + size_t index = ParseOperation(lock, operation, operations[i]); + std::string resource = parameters["Resource"].asString(); - if (resource.empty()) + if (!resource.empty()) { - previousCommand->ConnectOutput(command); + lock.AddDicomInstanceInput(index, context_, resource); } - else + else if (!isFirst) { - command.AddInput(resource); + lock.Connect(previous, index); } - previousCommand = &command; + isFirst = false; + previous = index; } - - job.SetDescription(description); - context_.GetScheduler().Submit(job); } - LuaScripting::LuaScripting(ServerContext& context) : context_(context) + LuaScripting::LuaScripting(ServerContext& context) : + context_(context), + state_(State_Setup) { lua_.SetGlobalVariable("_ServerContext", &context); lua_.RegisterFunction("RestApiGet", RestApiGet); @@ -391,34 +524,88 @@ lua_.RegisterFunction("RestApiPut", RestApiPut); lua_.RegisterFunction("RestApiDelete", RestApiDelete); lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration); + } - lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); + + LuaScripting::~LuaScripting() + { + if (state_ == State_Running) + { + LOG(ERROR) << "INTERNAL ERROR: LuaScripting::Stop() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } } - void LuaScripting::ApplyOnStoredInstance(const std::string& instanceId, - const Json::Value& simplifiedTags, - const Json::Value& metadata, - const DicomInstanceToStore& instance) + void LuaScripting::EventThread(LuaScripting* that) { - static const char* NAME = "OnStoredInstance"; + for (;;) + { + std::auto_ptr<IDynamicObject> event(that->pendingEvents_.Dequeue(100)); + + if (event.get() == NULL) + { + // The event queue is empty, check whether we should stop + boost::recursive_mutex::scoped_lock lock(that->mutex_); - if (lua_.IsExistingFunction(NAME)) - { - InitializeJob(); + if (that->state_ != State_Running) + { + return; + } + } + else + { + try + { + dynamic_cast<IEvent&>(*event).Apply(*that); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error while processing Lua events: " << e.What(); + } + } + } + } + + + void LuaScripting::Start() + { + boost::recursive_mutex::scoped_lock lock(mutex_); - LuaFunctionCall call(lua_, NAME); - call.PushString(instanceId); - call.PushJson(simplifiedTags); - call.PushJson(metadata); + if (state_ != State_Setup || + eventThread_.joinable() /* already started */) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + LOG(INFO) << "Starting the Lua engine"; + eventThread_ = boost::thread(EventThread, this); + state_ = State_Running; + } + } + - Json::Value origin; - instance.GetOriginInformation(origin); - call.PushJson(origin); + void LuaScripting::Stop() + { + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (state_ != State_Running) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } - call.Execute(); + state_ = State_Done; + } + + jobManager_.AwakeTrailingSleep(); - SubmitJob(std::string("Lua script: ") + NAME); + if (eventThread_.joinable()) + { + LOG(INFO) << "Stopping the Lua engine"; + eventThread_.join(); + LOG(INFO) << "The Lua engine has stopped"; } } @@ -427,8 +614,6 @@ DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { - boost::recursive_mutex::scoped_lock lock(mutex_); - Json::Value metadata = Json::objectValue; for (ServerIndex::MetadataMap::const_iterator @@ -441,72 +626,17 @@ } } - ApplyOnStoredInstance(publicId, simplifiedTags, metadata, instance); + pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance)); } - - void LuaScripting::OnStableResource(const ServerIndexChange& change) - { - const char* name; - - switch (change.GetChangeType()) - { - case ChangeType_StablePatient: - name = "OnStablePatient"; - break; - - case ChangeType_StableStudy: - name = "OnStableStudy"; - break; - - case ChangeType_StableSeries: - name = "OnStableSeries"; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - { - // Avoid unnecessary calls to the database if there's no Lua callback - boost::recursive_mutex::scoped_lock lock(mutex_); - if (!lua_.IsExistingFunction(name)) - { - return; - } - } - - Json::Value tags, metadata; - if (context_.GetIndex().LookupResource(tags, change.GetPublicId(), change.GetResourceType()) && - context_.GetIndex().GetMetadata(metadata, change.GetPublicId())) - { - boost::recursive_mutex::scoped_lock lock(mutex_); - - if (lua_.IsExistingFunction(name)) - { - InitializeJob(); - - LuaFunctionCall call(lua_, name); - call.PushString(change.GetPublicId()); - call.PushJson(tags["MainDicomTags"]); - call.PushJson(metadata); - call.Execute(); - - SubmitJob(std::string("Lua script: ") + name); - } - } - } - - - void LuaScripting::SignalChange(const ServerIndexChange& change) { if (change.GetChangeType() == ChangeType_StablePatient || change.GetChangeType() == ChangeType_StableStudy || change.GetChangeType() == ChangeType_StableSeries) { - OnStableResource(change); + pendingEvents_.Enqueue(new StableResourceEvent(change)); } } @@ -524,7 +654,7 @@ call.PushJson(simplified); Json::Value origin; - instance.GetOriginInformation(origin); + instance.GetOrigin().Format(origin); call.PushJson(origin); if (!call.ExecutePredicate()) @@ -539,12 +669,28 @@ void LuaScripting::Execute(const std::string& command) { - LuaScripting::Locker locker(*this); - - if (locker.GetLua().IsExistingFunction(command.c_str())) + pendingEvents_.Enqueue(new ExecuteEvent(command)); + } + + + void LuaScripting::LoadGlobalConfiguration() + { + lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); + + std::list<std::string> luaScripts; + Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts"); + + LuaScripting::Lock lock(*this); + + for (std::list<std::string>::const_iterator + it = luaScripts.begin(); it != luaScripts.end(); ++it) { - LuaFunctionCall call(locker.GetLua(), command.c_str()); - call.Execute(); + std::string path = Configuration::InterpretStringParameterAsPath(*it); + LOG(INFO) << "Installing the Lua scripts from: " << path; + std::string script; + SystemToolbox::ReadFile(script, path); + + lock.GetLua().Execute(script); } } }
--- a/OrthancServer/LuaScripting.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/LuaScripting.h Mon Jun 11 16:30:13 2018 +0200 @@ -34,8 +34,11 @@ #pragma once #include "IServerListener.h" + +#include "ServerJobs/LuaJobManager.h" + +#include "../Core/MultiThreading/SharedMessageQueue.h" #include "../Core/Lua/LuaContext.h" -#include "Scheduler/IServerCommand.h" namespace Orthanc { @@ -44,6 +47,18 @@ class LuaScripting : public IServerListener { private: + enum State + { + State_Setup, + State_Running, + State_Done + }; + + class ExecuteEvent; + class IEvent; + class OnStoredInstanceEvent; + class StableResourceEvent; + static ServerContext* GetServerContext(lua_State *state); static int RestApiPostOrPut(lua_State *state, @@ -54,33 +69,33 @@ static int RestApiDelete(lua_State *state); static int GetOrthancConfiguration(lua_State *state); - void ApplyOnStoredInstance(const std::string& instanceId, - const Json::Value& simplifiedDicom, - const Json::Value& metadata, - const DicomInstanceToStore& instance); - - IServerCommand* ParseOperation(const std::string& operation, - const Json::Value& parameters); + size_t ParseOperation(LuaJobManager::Lock& lock, + const std::string& operation, + const Json::Value& parameters); void InitializeJob(); - void SubmitJob(const std::string& description); - - void OnStableResource(const ServerIndexChange& change); + void SubmitJob(); - boost::recursive_mutex mutex_; - LuaContext lua_; - ServerContext& context_; + boost::recursive_mutex mutex_; + LuaContext lua_; + ServerContext& context_; + LuaJobManager jobManager_; + State state_; + boost::thread eventThread_; + SharedMessageQueue pendingEvents_; + + static void EventThread(LuaScripting* that); public: - class Locker : public boost::noncopyable + class Lock : public boost::noncopyable { private: - LuaScripting& that_; - boost::recursive_mutex::scoped_lock lock_; + LuaScripting& that_; + boost::recursive_mutex::scoped_lock lock_; public: - Locker(LuaScripting& that) : + explicit Lock(LuaScripting& that) : that_(that), lock_(that.mutex_) { @@ -93,6 +108,12 @@ }; LuaScripting(ServerContext& context); + + ~LuaScripting(); + + void Start(); + + void Stop(); virtual void SignalStoredInstance(const std::string& publicId, DicomInstanceToStore& instance, @@ -104,5 +125,7 @@ const Json::Value& simplifiedTags); void Execute(const std::string& command); + + void LoadGlobalConfiguration(); }; }
--- a/OrthancServer/OrthancFindRequestHandler.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -485,9 +485,10 @@ const std::string& calledAet) { static const char* LUA_CALLBACK = "IncomingFindRequestFilter"; + + LuaScripting::Lock lock(context_.GetLuaScripting()); - LuaScripting::Locker locker(context_.GetLua()); - if (!locker.GetLua().IsExistingFunction(LUA_CALLBACK)) + if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK)) { return false; } @@ -498,7 +499,7 @@ origin["RemoteAet"] = remoteAet; origin["CalledAet"] = calledAet; - LuaFunctionCall call(locker.GetLua(), LUA_CALLBACK); + LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK); call.PushDicom(source); call.PushJson(origin); FromDcmtkBridge::ExecuteToDicom(target, call); @@ -508,6 +509,14 @@ } + OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) : + context_(context), + maxResults_(0), + maxInstances_(0) + { + } + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::list<DicomTag>& sequencesToReturn,
--- a/OrthancServer/OrthancFindRequestHandler.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.h Mon Jun 11 16:30:13 2018 +0200 @@ -42,8 +42,8 @@ { private: ServerContext& context_; - unsigned int maxResults_; - unsigned int maxInstances_; + unsigned int maxResults_; + unsigned int maxInstances_; bool HasReachedLimit(const DicomFindAnswers& answers, ResourceType level) const; @@ -60,12 +60,7 @@ const std::string& calledAet); public: - OrthancFindRequestHandler(ServerContext& context) : - context_(context), - maxResults_(0), - maxInstances_(0) - { - } + OrthancFindRequestHandler(ServerContext& context); virtual void Handle(DicomFindAnswers& answers, const DicomMap& input,
--- a/OrthancServer/OrthancInitialization.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -960,7 +960,7 @@ peers.removeMember(symbolicName); Json::Value v; - peer.ToJson(v); + peer.ToJson(v, true); peers[symbolicName] = v; }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -38,6 +38,7 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" +#include "ServerJobs/DicomModalityStoreJob.h" namespace Orthanc { @@ -45,7 +46,7 @@ { // Anonymous namespace to avoid clashes between compilation modules - class OrthancMoveRequestIterator : public IMoveRequestIterator + class SynchronousMove : public IMoveRequestIterator { private: ServerContext& context_; @@ -55,20 +56,21 @@ RemoteModalityParameters remote_; std::string originatorAet_; uint16_t originatorId_; + std::auto_ptr<DicomUserConnection> connection_; public: - OrthancMoveRequestIterator(ServerContext& context, - const std::string& aet, - const std::string& publicId, - const std::string& originatorAet, - uint16_t originatorId) : + SynchronousMove(ServerContext& context, + const std::string& targetAet, + const std::string& publicId, + const std::string& originatorAet, + uint16_t originatorId) : context_(context), localAet_(context.GetDefaultLocalApplicationEntityTitle()), position_(0), originatorAet_(originatorAet), originatorId_(originatorId) { - LOG(INFO) << "Sending resource " << publicId << " to modality \"" << aet << "\""; + LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\""; std::list<std::string> tmp; context_.GetIndex().GetChildInstances(tmp, publicId); @@ -79,7 +81,7 @@ instances_.push_back(*it); } - remote_ = Configuration::GetModalityUsingAet(aet); + remote_ = Configuration::GetModalityUsingAet(targetAet); } virtual unsigned int GetSubOperationCount() const @@ -99,15 +101,76 @@ std::string dicom; context_.ReadDicom(dicom, id); + if (connection_.get() == NULL) { - ReusableDicomUserConnection::Locker locker - (context_.GetReusableDicomUserConnection(), localAet_, remote_); - locker.GetConnection().Store(dicom, originatorAet_, originatorId_); + connection_.reset(new DicomUserConnection(localAet_, remote_)); } + connection_->Store(dicom, originatorAet_, originatorId_); + return Status_Success; } }; + + + class AsynchronousMove : public IMoveRequestIterator + { + private: + ServerContext& context_; + std::auto_ptr<DicomModalityStoreJob> job_; + size_t position_; + + public: + AsynchronousMove(ServerContext& context, + const std::string& targetAet, + const std::string& publicId, + const std::string& originatorAet, + uint16_t originatorId) : + context_(context), + job_(new DicomModalityStoreJob(context)), + position_(0) + { + LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\""; + + job_->SetDescription("C-MOVE"); + job_->SetPermissive(true); + job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle()); + job_->SetRemoteModality(Configuration::GetModalityUsingAet(targetAet)); + + if (originatorId != 0) + { + job_->SetMoveOriginator(originatorAet, originatorId); + } + + std::list<std::string> tmp; + context_.GetIndex().GetChildInstances(tmp, publicId); + + job_->Reserve(tmp.size()); + + for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it) + { + job_->AddInstance(*it); + } + } + + virtual unsigned int GetSubOperationCount() const + { + return 1; + } + + virtual Status DoNext() + { + if (position_ == 0) + { + context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */); + return Status_Success; + } + else + { + return Status_Failure; + } + } + }; } @@ -169,6 +232,25 @@ } + static IMoveRequestIterator* CreateIterator(ServerContext& context, + const std::string& targetAet, + const std::string& publicId, + const std::string& originatorAet, + uint16_t originatorId) + { + bool synchronous = Configuration::GetGlobalBoolParameter("SynchronousCMove", false); + + if (synchronous) + { + return new SynchronousMove(context, targetAet, publicId, originatorAet, originatorId); + } + else + { + return new AsynchronousMove(context, targetAet, publicId, originatorAet, originatorId); + } + } + + IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet, const DicomMap& input, const std::string& originatorIp, @@ -191,7 +273,6 @@ } } - /** * Retrieve the query level. **/ @@ -215,7 +296,7 @@ LookupIdentifier(publicId, ResourceType_Study, input) || LookupIdentifier(publicId, ResourceType_Patient, input)) { - return new OrthancMoveRequestIterator(context_, targetAet, publicId, originatorAet, originatorId); + return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId); } else { @@ -236,7 +317,7 @@ if (LookupIdentifier(publicId, level, input)) { - return new OrthancMoveRequestIterator(context_, targetAet, publicId, originatorAet, originatorId); + return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId); } else {
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,10 +34,10 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../ServerContext.h" -#include "../OrthancInitialization.h" +#include "../ServerJobs/ResourceModificationJob.h" #include <boost/lexical_cast.hpp> #include <boost/algorithm/string/predicate.hpp> @@ -54,14 +54,39 @@ } + static int GetPriority(const Json::Value& request) + { + static const char* PRIORITY = "Priority"; + + if (request.isMember(PRIORITY)) + { + if (request[PRIORITY].type() == Json::intValue) + { + return request[PRIORITY].asInt(); + } + else + { + LOG(ERROR) << "Field \"" << PRIORITY << "\" of a modification request should be an integer"; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else + { + return 0; // Default priority + } + } + + static void ParseModifyRequest(DicomModification& target, + int& priority, const RestApiPostCall& call) { - // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}' + // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}' Json::Value request; if (call.ParseJsonRequest(request)) { + priority = GetPriority(request); target.ParseModifyRequest(request); } else @@ -72,6 +97,7 @@ static void ParseAnonymizationRequest(DicomModification& target, + int& priority, RestApiPostCall& call) { // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm @@ -80,6 +106,8 @@ if (call.ParseJsonRequest(request) && request.isObject()) { + priority = GetPriority(request); + bool patientNameReplaced; target.ParseAnonymizationRequest(patientNameReplaced, request); @@ -110,142 +138,36 @@ } - static void AnonymizeOrModifyResource(DicomModification& modification, - MetadataType metadataType, - ChangeType changeType, - ResourceType resourceType, - RestApiPostCall& call) + + static void SubmitJob(std::auto_ptr<DicomModification>& modification, + bool isAnonymization, + ResourceType level, + int priority, + RestApiPostCall& call) { - bool isFirst = true; - Json::Value result(Json::objectValue); - ServerContext& context = OrthancRestApi::GetContext(call); - typedef std::list<std::string> Instances; - Instances instances; - std::string id = call.GetUriComponent("id", ""); - context.GetIndex().GetChildInstances(instances, id); - - if (instances.empty()) - { - return; - } - - - /** - * Loop over all the instances of the resource. - **/ - - for (Instances::const_iterator it = instances.begin(); - it != instances.end(); ++it) - { - LOG(INFO) << "Modifying instance " << *it; - - std::auto_ptr<ServerContext::DicomCacheLocker> locker; - - try - { - locker.reset(new ServerContext::DicomCacheLocker(OrthancRestApi::GetContext(call), *it)); - } - catch (OrthancException&) - { - // This child instance has been removed in between - continue; - } - - - ParsedDicomFile& original = locker->GetDicom(); - DicomInstanceHasher originalHasher = original.GetHasher(); - - - /** - * Compute the resulting DICOM instance. - **/ - - std::auto_ptr<ParsedDicomFile> modified(original.Clone(true)); - modification.Apply(*modified); - - DicomInstanceToStore toStore; - toStore.SetRestOrigin(call); - toStore.SetParsedDicomFile(*modified); - - - /** - * Prepare the metadata information to associate with the - * resulting DICOM instance (AnonymizedFrom/ModifiedFrom). - **/ - - DicomInstanceHasher modifiedHasher = modified->GetHasher(); - - if (originalHasher.HashSeries() != modifiedHasher.HashSeries()) - { - toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries()); - } + std::auto_ptr<ResourceModificationJob> job(new ResourceModificationJob(context)); + + boost::shared_ptr<ResourceModificationJob::Output> output(new ResourceModificationJob::Output(level)); + job->SetModification(modification.release(), isAnonymization); + job->SetOutput(output); + job->SetOrigin(call); + job->SetDescription("REST API"); - if (originalHasher.HashStudy() != modifiedHasher.HashStudy()) - { - toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy()); - } - - if (originalHasher.HashPatient() != modifiedHasher.HashPatient()) - { - toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient()); - } - - assert(*it == originalHasher.HashInstance()); - toStore.AddMetadata(ResourceType_Instance, metadataType, *it); - - - /** - * Store the resulting DICOM instance into the Orthanc store. - **/ - - std::string modifiedInstance; - if (context.Store(modifiedInstance, toStore) != StoreStatus_Success) + context.AddChildInstances(*job, call.GetUriComponent("id", "")); + + if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority)) + { + Json::Value json; + if (output->Format(json)) { - LOG(ERROR) << "Error while storing a modified instance " << *it; - throw OrthancException(ErrorCode_CannotStoreInstance); - } - - // Sanity checks in debug mode - assert(modifiedInstance == modifiedHasher.HashInstance()); - - - /** - * Compute the JSON object that is returned by the REST call. - **/ - - if (isFirst) - { - std::string newId; - - switch (resourceType) - { - case ResourceType_Series: - newId = modifiedHasher.HashSeries(); - break; - - case ResourceType_Study: - newId = modifiedHasher.HashStudy(); - break; - - case ResourceType_Patient: - newId = modifiedHasher.HashPatient(); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - result["Type"] = EnumerationToString(resourceType); - result["ID"] = newId; - result["Path"] = GetBasePath(resourceType, newId); - result["PatientID"] = modifiedHasher.HashPatient(); - isFirst = false; + call.GetOutput().AnswerJson(json); + return; } } - call.GetOutput().AnswerJson(result); + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } @@ -255,7 +177,8 @@ DicomModification modification; modification.SetAllowManualIdentifiers(true); - ParseModifyRequest(modification, call); + int priority; + ParseModifyRequest(modification, priority, call); if (modification.IsReplaced(DICOM_TAG_PATIENT_ID)) { @@ -283,36 +206,35 @@ DicomModification modification; modification.SetAllowManualIdentifiers(true); - ParseAnonymizationRequest(modification, call); + int priority; + ParseAnonymizationRequest(modification, priority, call); AnonymizeOrModifyInstance(modification, call); } - template <enum ChangeType changeType, - enum ResourceType resourceType> + template <enum ResourceType resourceType> static void ModifyResource(RestApiPostCall& call) { - DicomModification modification; - - ParseModifyRequest(modification, call); + std::auto_ptr<DicomModification> modification(new DicomModification); - modification.SetLevel(resourceType); - AnonymizeOrModifyResource(modification, MetadataType_ModifiedFrom, - changeType, resourceType, call); + int priority; + ParseModifyRequest(*modification, priority, call); + + modification->SetLevel(resourceType); + SubmitJob(modification, false, resourceType, priority, call); } - template <enum ChangeType changeType, - enum ResourceType resourceType> + template <enum ResourceType resourceType> static void AnonymizeResource(RestApiPostCall& call) { - DicomModification modification; + std::auto_ptr<DicomModification> modification(new DicomModification); - ParseAnonymizationRequest(modification, call); + int priority; + ParseAnonymizationRequest(*modification, priority, call); - AnonymizeOrModifyResource(modification, MetadataType_AnonymizedFrom, - changeType, resourceType, call); + SubmitJob(modification, true, resourceType, priority, call); } @@ -321,7 +243,7 @@ ParsedDicomFile& dicom) { DicomInstanceToStore toStore; - toStore.SetRestOrigin(call); + toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); toStore.SetParsedDicomFile(dicom); ServerContext& context = OrthancRestApi::GetContext(call); @@ -729,14 +651,14 @@ void OrthancRestApi::RegisterAnonymizeModify() { Register("/instances/{id}/modify", ModifyInstance); - Register("/series/{id}/modify", ModifyResource<ChangeType_ModifiedSeries, ResourceType_Series>); - Register("/studies/{id}/modify", ModifyResource<ChangeType_ModifiedStudy, ResourceType_Study>); - Register("/patients/{id}/modify", ModifyResource<ChangeType_ModifiedPatient, ResourceType_Patient>); + Register("/series/{id}/modify", ModifyResource<ResourceType_Series>); + Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>); + Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>); Register("/instances/{id}/anonymize", AnonymizeInstance); - Register("/series/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedSeries, ResourceType_Series>); - Register("/studies/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedStudy, ResourceType_Study>); - Register("/patients/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedPatient, ResourceType_Patient>); + Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>); + Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>); + Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>); Register("/tools/create-dicom", CreateDicom); }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -93,7 +93,7 @@ std::string postData(call.GetBodyData(), call.GetBodySize()); DicomInstanceToStore toStore; - toStore.SetRestOrigin(call); + toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); toStore.SetBuffer(postData); std::string publicId;
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,651 +34,14 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" -#include "../../Core/DicomParsing/DicomDirWriter.h" -#include "../../Core/FileStorage/StorageAccessor.h" -#include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" -#include "../../Core/Logging.h" -#include "../../Core/TemporaryFile.h" -#include "../ServerContext.h" - -#include <stdio.h> - -#if defined(_MSC_VER) -#define snprintf _snprintf -#endif - -static const uint64_t MEGA_BYTES = 1024 * 1024; -static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024; +#include "../ServerJobs/ArchiveJob.h" namespace Orthanc { - // Download of ZIP files ---------------------------------------------------- - - static bool IsZip64Required(uint64_t uncompressedSize, - unsigned int countInstances) - { - static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; - - /** - * Determine whether ZIP64 is required. Original ZIP format can - * store up to 2GB of data (some implementation supporting up to - * 4GB of data), and up to 65535 files. - * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64 - **/ - - const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN || - countInstances >= 65535); - - LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size " - << (uncompressedSize / MEGA_BYTES) << "MB using the " - << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; - - return isZip64; - } - - - namespace - { - class ResourceIdentifiers - { - private: - ResourceType level_; - std::string patient_; - std::string study_; - std::string series_; - std::string instance_; - - static void GoToParent(ServerIndex& index, - std::string& current) - { - std::string tmp; - - if (index.LookupParent(tmp, current)) - { - current = tmp; - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - public: - ResourceIdentifiers(ServerIndex& index, - const std::string& publicId) - { - if (!index.LookupResourceType(level_, publicId)) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - std::string current = publicId;; - switch (level_) // Do not add "break" below! - { - case ResourceType_Instance: - instance_ = current; - GoToParent(index, current); - - case ResourceType_Series: - series_ = current; - GoToParent(index, current); - - case ResourceType_Study: - study_ = current; - GoToParent(index, current); - - case ResourceType_Patient: - patient_ = current; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - ResourceType GetLevel() const - { - return level_; - } - - const std::string& GetIdentifier(ResourceType level) const - { - // Some sanity check to ensure enumerations are not altered - assert(ResourceType_Patient < ResourceType_Study); - assert(ResourceType_Study < ResourceType_Series); - assert(ResourceType_Series < ResourceType_Instance); - - if (level > level_) - { - throw OrthancException(ErrorCode_InternalError); - } - - switch (level) - { - case ResourceType_Patient: - return patient_; - - case ResourceType_Study: - return study_; - - case ResourceType_Series: - return series_; - - case ResourceType_Instance: - return instance_; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - }; - - - class IArchiveVisitor : public boost::noncopyable - { - public: - virtual ~IArchiveVisitor() - { - } - - virtual void Open(ResourceType level, - const std::string& publicId) = 0; - - virtual void Close() = 0; - - virtual void AddInstance(const std::string& instanceId, - const FileInfo& dicom) = 0; - }; - - - class ArchiveIndex - { - private: - struct Instance - { - std::string id_; - FileInfo dicom_; - - Instance(const std::string& id, - const FileInfo& dicom) : - id_(id), dicom_(dicom) - { - } - }; - - // A "NULL" value for ArchiveIndex indicates a non-expanded node - typedef std::map<std::string, ArchiveIndex*> Resources; - - ResourceType level_; - Resources resources_; // Only at patient/study/series level - std::list<Instance> instances_; // Only at instance level - - - void AddResourceToExpand(ServerIndex& index, - const std::string& id) - { - if (level_ == ResourceType_Instance) - { - FileInfo tmp; - if (index.LookupAttachment(tmp, id, FileContentType_Dicom)) - { - instances_.push_back(Instance(id, tmp)); - } - } - else - { - resources_[id] = NULL; - } - } - - - public: - ArchiveIndex(ResourceType level) : - level_(level) - { - } - - ~ArchiveIndex() - { - for (Resources::iterator it = resources_.begin(); - it != resources_.end(); ++it) - { - delete it->second; - } - } - - - void Add(ServerIndex& index, - const ResourceIdentifiers& resource) - { - const std::string& id = resource.GetIdentifier(level_); - Resources::iterator previous = resources_.find(id); - - if (level_ == ResourceType_Instance) - { - AddResourceToExpand(index, id); - } - else if (resource.GetLevel() == level_) - { - // Mark this resource for further expansion - if (previous != resources_.end()) - { - delete previous->second; - } - - resources_[id] = NULL; - } - else if (previous == resources_.end()) - { - // This is the first time we meet this resource - std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_))); - child->Add(index, resource); - resources_[id] = child.release(); - } - else if (previous->second != NULL) - { - previous->second->Add(index, resource); - } - else - { - // Nothing to do: This item is marked for further expansion - } - } - - - void Expand(ServerIndex& index) - { - if (level_ == ResourceType_Instance) - { - // Expanding an instance node makes no sense - return; - } - - for (Resources::iterator it = resources_.begin(); - it != resources_.end(); ++it) - { - if (it->second == NULL) - { - // This is resource is marked for expansion - std::list<std::string> children; - index.GetChildren(children, it->first); - - std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_))); - - for (std::list<std::string>::const_iterator - it2 = children.begin(); it2 != children.end(); ++it2) - { - child->AddResourceToExpand(index, *it2); - } - - it->second = child.release(); - } - - assert(it->second != NULL); - it->second->Expand(index); - } - } - - - void Apply(IArchiveVisitor& visitor) const - { - if (level_ == ResourceType_Instance) - { - for (std::list<Instance>::const_iterator - it = instances_.begin(); it != instances_.end(); ++it) - { - visitor.AddInstance(it->id_, it->dicom_); - } - } - else - { - for (Resources::const_iterator it = resources_.begin(); - it != resources_.end(); ++it) - { - assert(it->second != NULL); // There must have been a call to "Expand()" - visitor.Open(level_, it->first); - it->second->Apply(visitor); - visitor.Close(); - } - } - } - }; - - - class StatisticsVisitor : public IArchiveVisitor - { - private: - uint64_t size_; - unsigned int instances_; - - public: - StatisticsVisitor() : size_(0), instances_(0) - { - } - - uint64_t GetUncompressedSize() const - { - return size_; - } - - unsigned int GetInstancesCount() const - { - return instances_; - } - - virtual void Open(ResourceType level, - const std::string& publicId) - { - } - - virtual void Close() - { - } - - virtual void AddInstance(const std::string& instanceId, - const FileInfo& dicom) - { - instances_ ++; - size_ += dicom.GetUncompressedSize(); - } - }; - - - class PrintVisitor : public IArchiveVisitor - { - private: - std::ostream& out_; - std::string indent_; - - public: - PrintVisitor(std::ostream& out) : out_(out) - { - } - - virtual void Open(ResourceType level, - const std::string& publicId) - { - switch (level) - { - case ResourceType_Patient: indent_ = ""; break; - case ResourceType_Study: indent_ = " "; break; - case ResourceType_Series: indent_ = " "; break; - default: - throw OrthancException(ErrorCode_InternalError); - } - - out_ << indent_ << publicId << std::endl; - } - - virtual void Close() - { - } - - virtual void AddInstance(const std::string& instanceId, - const FileInfo& dicom) - { - out_ << " " << instanceId << std::endl; - } - }; - - - class ArchiveWriterVisitor : public IArchiveVisitor - { - private: - HierarchicalZipWriter& writer_; - ServerContext& context_; - char instanceFormat_[24]; - unsigned int countInstances_; - - static std::string GetTag(const DicomMap& tags, - const DicomTag& tag) - { - const DicomValue* v = tags.TestAndGetValue(tag); - if (v != NULL && - !v->IsBinary() && - !v->IsNull()) - { - return v->GetContent(); - } - else - { - return ""; - } - } - - public: - ArchiveWriterVisitor(HierarchicalZipWriter& writer, - ServerContext& context) : - writer_(writer), - context_(context), - countInstances_(0) - { - snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); - } - - virtual void Open(ResourceType level, - const std::string& publicId) - { - std::string path; - - DicomMap tags; - if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level)) - { - switch (level) - { - case ResourceType_Patient: - path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME); - break; - - case ResourceType_Study: - path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION); - break; - - case ResourceType_Series: - { - std::string modality = GetTag(tags, DICOM_TAG_MODALITY); - path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION); - - if (modality.size() == 0) - { - snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); - } - else if (modality.size() == 1) - { - snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", - toupper(modality[0])); - } - else if (modality.size() >= 2) - { - snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", - toupper(modality[0]), toupper(modality[1])); - } - - countInstances_ = 0; - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path)); - - if (path.empty()) - { - path = std::string("Unknown ") + EnumerationToString(level); - } - - writer_.OpenDirectory(path.c_str()); - } - - virtual void Close() - { - writer_.CloseDirectory(); - } - - virtual void AddInstance(const std::string& instanceId, - const FileInfo& dicom) - { - std::string content; - context_.ReadAttachment(content, dicom); - - char filename[24]; - snprintf(filename, sizeof(filename) - 1, instanceFormat_, countInstances_); - countInstances_ ++; - - writer_.OpenFile(filename); - writer_.Write(content); - } - - static void Apply(RestApiOutput& output, - ServerContext& context, - ArchiveIndex& archive, - const std::string& filename) - { - archive.Expand(context.GetIndex()); - - StatisticsVisitor stats; - archive.Apply(stats); - - const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount()); - - // Create a RAII for the temporary file to manage the ZIP file - TemporaryFile tmp; - - { - // Create a ZIP writer - HierarchicalZipWriter writer(tmp.GetPath().c_str()); - writer.SetZip64(isZip64); - - ArchiveWriterVisitor v(writer, context); - archive.Apply(v); - } - - // Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp.GetPath()); - sender.SetContentType("application/zip"); - sender.SetContentFilename(filename); - - // Send the ZIP - output.AnswerStream(sender); - - // The temporary file is automatically removed thanks to the RAII - } - }; - - - class MediaWriterVisitor : public IArchiveVisitor - { - private: - HierarchicalZipWriter& writer_; - DicomDirWriter dicomDir_; - ServerContext& context_; - unsigned int countInstances_; - - public: - MediaWriterVisitor(HierarchicalZipWriter& writer, - ServerContext& context) : - writer_(writer), - context_(context), - countInstances_(0) - { - } - - void EncodeDicomDir(std::string& result) - { - dicomDir_.Encode(result); - } - - virtual void Open(ResourceType level, - const std::string& publicId) - { - } - - virtual void Close() - { - } - - virtual void AddInstance(const std::string& instanceId, - const FileInfo& dicom) - { - // "DICOM restricts the filenames on DICOM media to 8 - // characters (some systems wrongly use 8.3, but this does not - // conform to the standard)." - std::string filename = "IM" + boost::lexical_cast<std::string>(countInstances_); - writer_.OpenFile(filename.c_str()); - - std::string content; - context_.ReadAttachment(content, dicom); - writer_.Write(content); - - ParsedDicomFile parsed(content); - dicomDir_.Add("IMAGES", filename, parsed); - - countInstances_ ++; - } - - static void Apply(RestApiOutput& output, - ServerContext& context, - ArchiveIndex& archive, - const std::string& filename, - bool enableExtendedSopClass) - { - archive.Expand(context.GetIndex()); - - StatisticsVisitor stats; - archive.Apply(stats); - - const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount()); - - // Create a RAII for the temporary file to manage the ZIP file - TemporaryFile tmp; - - { - // Create a ZIP writer - HierarchicalZipWriter writer(tmp.GetPath().c_str()); - writer.SetZip64(isZip64); - writer.OpenDirectory("IMAGES"); - - // Create a DICOMDIR writer - MediaWriterVisitor v(writer, context); - - // Request type-3 arguments to be added to the DICOMDIR - v.dicomDir_.EnableExtendedSopClass(enableExtendedSopClass); - - archive.Apply(v); - - // Add the DICOMDIR - writer.CloseDirectory(); - writer.OpenFile("DICOMDIR"); - std::string s; - v.EncodeDicomDir(s); - writer.Write(s); - } - - // Prepare the sending of the ZIP file - FilesystemHttpSender sender(tmp.GetPath()); - sender.SetContentType("application/zip"); - sender.SetContentFilename(filename); - - // Send the ZIP - output.AnswerStream(sender); - - // The temporary file is automatically removed thanks to the RAII - } - }; - } - - - static bool AddResourcesOfInterest(ArchiveIndex& archive, + static bool AddResourcesOfInterest(ArchiveJob& job, RestApiPostCall& call) { - ServerIndex& index = OrthancRestApi::GetIndex(call); - Json::Value resources; if (call.ParseJsonRequest(resources) && resources.type() == Json::arrayValue) @@ -690,8 +53,7 @@ return false; // Bad request } - ResourceIdentifiers resource(index, resources[i].asString()); - archive.Add(index, resource); + job.AddResource(resources[i].asString()); } return true; @@ -703,16 +65,46 @@ } + static void SubmitJob(RestApiCall& call, + boost::shared_ptr<TemporaryFile>& tmp, + ServerContext& context, + std::auto_ptr<ArchiveJob>& job, + const std::string& filename) + { + if (job.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + job->SetDescription("REST API"); + + if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), 0 /* TODO priority */)) + { + // The archive is now created: Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp->GetPath()); + sender.SetContentType("application/zip"); + sender.SetContentFilename(filename); + + // Send the ZIP + call.GetOutput().AnswerStream(sender); + } + else + { + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } + } + + static void CreateBatchArchive(RestApiPostCall& call) { - ArchiveIndex archive(ResourceType_Patient); // root + ServerContext& context = OrthancRestApi::GetContext(call); - if (AddResourcesOfInterest(archive, call)) + boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile); + std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false)); + + if (AddResourcesOfInterest(*job, call)) { - ArchiveWriterVisitor::Apply(call.GetOutput(), - OrthancRestApi::GetContext(call), - archive, - "Archive.zip"); + SubmitJob(call, tmp, context, job, "Archive.zip"); } } @@ -720,51 +112,43 @@ template <bool Extended> static void CreateBatchMedia(RestApiPostCall& call) { - ArchiveIndex archive(ResourceType_Patient); // root + ServerContext& context = OrthancRestApi::GetContext(call); - if (AddResourcesOfInterest(archive, call)) + boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile); + std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, Extended)); + + if (AddResourcesOfInterest(*job, call)) { - MediaWriterVisitor::Apply(call.GetOutput(), - OrthancRestApi::GetContext(call), - archive, - "Archive.zip", - Extended); + SubmitJob(call, tmp, context, job, "Archive.zip"); } - } - + } + static void CreateArchive(RestApiGetCall& call) { - ServerIndex& index = OrthancRestApi::GetIndex(call); + ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); - ResourceIdentifiers resource(index, id); - - ArchiveIndex archive(ResourceType_Patient); // root - archive.Add(OrthancRestApi::GetIndex(call), resource); - ArchiveWriterVisitor::Apply(call.GetOutput(), - OrthancRestApi::GetContext(call), - archive, - id + ".zip"); + boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile); + std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false)); + job->AddResource(id); + + SubmitJob(call, tmp, context, job, id + ".zip"); } static void CreateMedia(RestApiGetCall& call) { - ServerIndex& index = OrthancRestApi::GetIndex(call); + ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); - ResourceIdentifiers resource(index, id); - - ArchiveIndex archive(ResourceType_Patient); // root - archive.Add(OrthancRestApi::GetIndex(call), resource); - MediaWriterVisitor::Apply(call.GetOutput(), - OrthancRestApi::GetContext(call), - archive, - id + ".zip", - call.HasArgument("extended")); + boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile); + std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, call.HasArgument("extended"))); + job->AddResource(id); + + SubmitJob(call, tmp, context, job, id + ".zip"); }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,16 +34,15 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" -#include "../OrthancInitialization.h" -#include "../../Core/HttpClient.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" -#include "../Scheduler/ServerJob.h" -#include "../Scheduler/StoreScuCommand.h" -#include "../Scheduler/StorePeerCommand.h" +#include "../OrthancInitialization.h" #include "../QueryRetrieveHandler.h" +#include "../ServerJobs/DicomModalityStoreJob.h" +#include "../ServerJobs/OrthancPeerStoreJob.h" #include "../ServerToolbox.h" + namespace Orthanc { /*************************************************************************** @@ -55,12 +54,15 @@ ServerContext& context = OrthancRestApi::GetContext(call); const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); try { - if (locker.GetConnection().Echo()) + DicomUserConnection connection(localAet, remote); + connection.Open(); + + if (connection.Echo()) { // Echo has succeeded call.GetOutput().AnswerBuffer("{}", "application/json"); @@ -176,11 +178,16 @@ } const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + DicomFindAnswers answers(false); - DicomFindAnswers answers(false); - FindPatient(answers, locker.GetConnection(), fields); + { + DicomUserConnection connection(localAet, remote); + connection.Open(); + FindPatient(answers, connection, fields); + } Json::Value result; answers.ToJson(result, true); @@ -206,11 +213,16 @@ } const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); - FindStudy(answers, locker.GetConnection(), fields); + + { + DicomUserConnection connection(localAet, remote); + connection.Open(); + FindStudy(answers, connection, fields); + } Json::Value result; answers.ToJson(result, true); @@ -237,11 +249,16 @@ } const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); - FindSeries(answers, locker.GetConnection(), fields); + + { + DicomUserConnection connection(localAet, remote); + connection.Open(); + FindSeries(answers, connection, fields); + } Json::Value result; answers.ToJson(result, true); @@ -269,11 +286,16 @@ } const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomFindAnswers answers(false); - FindInstance(answers, locker.GetConnection(), fields); + + { + DicomUserConnection connection(localAet, remote); + connection.Open(); + FindInstance(answers, connection, fields); + } Json::Value result; answers.ToJson(result, true); @@ -306,11 +328,14 @@ } const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + DicomUserConnection connection(localAet, remote); + connection.Open(); + DicomFindAnswers patients(false); - FindPatient(patients, locker.GetConnection(), m); + FindPatient(patients, connection, m); // Loop over the found patients Json::Value result = Json::arrayValue; @@ -328,7 +353,7 @@ CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); DicomFindAnswers studies(false); - FindStudy(studies, locker.GetConnection(), m); + FindStudy(studies, connection, m); patient["Studies"] = Json::arrayValue; @@ -348,7 +373,7 @@ CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); DicomFindAnswers series(false); - FindSeries(series, locker.GetConnection(), m); + FindSeries(series, connection, m); // Loop over the found series study["Series"] = Json::arrayValue; @@ -582,7 +607,7 @@ ***************************************************************************/ static bool GetInstancesToExport(Json::Value& otherArguments, - std::list<std::string>& instances, + SetOfInstancesJob& job, const std::string& remote, RestApiPostCall& call) { @@ -656,21 +681,55 @@ { context.GetIndex().LogExportedResource(stripped, remote); } - - std::list<std::string> tmp; - context.GetIndex().GetChildInstances(tmp, stripped); - for (std::list<std::string>::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - instances.push_back(*it); - } + context.AddChildInstances(job, stripped); } return true; } + static void SubmitJob(RestApiPostCall& call, + const Json::Value& request, + SetOfInstancesJob* jobRaw) + { + std::auto_ptr<SetOfInstancesJob> job(jobRaw); + + if (job.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + ServerContext& context = OrthancRestApi::GetContext(call); + + bool permissive = Toolbox::GetJsonBooleanField(request, "Permissive", true); + bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false); + int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0); + + job->SetPermissive(permissive); + + if (asynchronous) + { + // Asynchronous mode: Submit the job, but don't wait for its completion + std::string id; + context.GetJobsEngine().GetRegistry().Submit(id, job.release(), priority); + + Json::Value v; + v["ID"] = id; + call.GetOutput().AnswerJson(v); + } + else if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority)) + { + // Synchronous mode: We have submitted and waited for completion + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } + } + + static void DicomStore(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -678,56 +737,29 @@ std::string remote = call.GetUriComponent("id", ""); Json::Value request; - std::list<std::string> instances; - if (!GetInstancesToExport(request, instances, remote, call)) - { - return; - } - - std::string localAet = Toolbox::GetJsonStringField(request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); - bool permissive = Toolbox::GetJsonBooleanField(request, "Permissive", false); - bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false); - std::string moveOriginatorAET = Toolbox::GetJsonStringField(request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle()); - int moveOriginatorID = Toolbox::GetJsonIntegerField(request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */); + std::auto_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context)); - if (moveOriginatorID < 0 || - moveOriginatorID >= 65536) + if (GetInstancesToExport(request, *job, remote, call)) { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote); + std::string localAet = Toolbox::GetJsonStringField + (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + std::string moveOriginatorAET = Toolbox::GetJsonStringField + (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle()); + int moveOriginatorID = Toolbox::GetJsonIntegerField + (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */); - ServerJob job; - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::auto_ptr<StoreScuCommand> command(new StoreScuCommand(context, localAet, p, permissive)); + RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote); + + job->SetDescription("REST API"); + job->SetLocalAet(localAet); + job->SetRemoteModality(p); if (moveOriginatorID != 0) { - command->SetMoveOriginator(moveOriginatorAET, static_cast<uint16_t>(moveOriginatorID)); + job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID); } - job.AddCommand(command.release()).AddInput(*it); - } - - job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\""); - - if (asynchronous) - { - // Asynchronous mode: Submit the job, but don't wait for its completion - context.GetScheduler().Submit(job); - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - else if (context.GetScheduler().SubmitAndWait(job)) - { - // Synchronous mode: We have submitted and waited for completion - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - else - { - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + SubmitJob(call, request, job.release()); } } @@ -757,18 +789,23 @@ ResourceType level = StringToResourceType(request["Level"].asCString()); - std::string localAet = Toolbox::GetJsonStringField(request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); - std::string targetAet = Toolbox::GetJsonStringField(request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle()); + std::string localAet = Toolbox::GetJsonStringField + (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + std::string targetAet = Toolbox::GetJsonStringField + (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle()); - const RemoteModalityParameters source = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - + const RemoteModalityParameters source = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + DicomUserConnection connection(localAet, source); + connection.Open(); + for (Json::Value::ArrayIndex i = 0; i < request[RESOURCES].size(); i++) { DicomMap resource; FromDcmtkBridge::FromJson(resource, request[RESOURCES][i]); - - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, source); - locker.GetConnection().Move(targetAet, level, resource); + + connection.Move(targetAet, level, resource); } // Move has succeeded @@ -844,40 +881,17 @@ std::string remote = call.GetUriComponent("id", ""); Json::Value request; - std::list<std::string> instances; - if (!GetInstancesToExport(request, instances, remote, call)) - { - return; - } + std::auto_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context)); - bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false); - - WebServiceParameters peer; - Configuration::GetOrthancPeer(peer, remote); - - ServerJob job; - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) + if (GetInstancesToExport(request, *job, remote, call)) { - job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it); - } - - job.SetDescription("HTTP request: POST to peer \"" + remote + "\""); - - if (asynchronous) - { - // Asynchronous mode: Submit the job, but don't wait for its completion - context.GetScheduler().Submit(job); - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - else if (context.GetScheduler().SubmitAndWait(job)) - { - // Synchronous mode: We have submitted and waited for completion - call.GetOutput().AnswerBuffer("{}", "application/json"); - } - else - { - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + WebServiceParameters peer; + Configuration::GetOrthancPeer(peer, remote); + + job->SetDescription("REST API"); + job->SetPeer(peer); + + SubmitJob(call, request, job.release()); } } @@ -984,15 +998,17 @@ if (call.ParseJsonRequest(json)) { const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + RemoteModalityParameters remote = + Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); std::auto_ptr<ParsedDicomFile> query(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0))); DicomFindAnswers answers(true); { - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); - locker.GetConnection().FindWorklist(answers, *query); + DicomUserConnection connection(localAet, remote); + connection.Open(); + connection.FindWorklist(answers, *query); } Json::Value result;
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -124,8 +124,8 @@ call.BodyToString(command); { - LuaScripting::Locker locker(context.GetLua()); - locker.GetLua().Execute(result, command); + LuaScripting::Lock lock(context.GetLuaScripting()); + lock.GetLua().Execute(result, command); } call.GetOutput().AnswerBuffer(result, "text/plain"); @@ -146,6 +146,24 @@ } + static void GetDefaultEncoding(RestApiGetCall& call) + { + Encoding encoding = GetDefaultDicomEncoding(); + call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + } + + + static void SetDefaultEncoding(RestApiPutCall& call) + { + Encoding encoding = StringToEncoding(call.GetBodyData()); + + Configuration::SetDefaultEncoding(encoding); + + call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + } + + + // Plugins information ------------------------------------------------------ static void ListPlugins(RestApiGetCall& call) @@ -251,23 +269,99 @@ } - static void GetDefaultEncoding(RestApiGetCall& call) + + + // Jobs information ------------------------------------------------------ + + static void ListJobs(RestApiGetCall& call) { - Encoding encoding = GetDefaultDicomEncoding(); - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + bool expand = call.HasArgument("expand"); + + Json::Value v = Json::arrayValue; + + std::set<std::string> jobs; + OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs); + + for (std::set<std::string>::const_iterator it = jobs.begin(); + it != jobs.end(); ++it) + { + if (expand) + { + JobInfo info; + if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, *it)) + { + Json::Value tmp; + info.Format(tmp); + v.append(tmp); + } + } + else + { + v.append(*it); + } + } + + call.GetOutput().AnswerJson(v); + } + + static void GetJobInfo(RestApiGetCall& call) + { + std::string id = call.GetUriComponent("id", ""); + + JobInfo info; + if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id)) + { + Json::Value json; + info.Format(json); + call.GetOutput().AnswerJson(json); + } } - static void SetDefaultEncoding(RestApiPutCall& call) + enum JobAction { - Encoding encoding = StringToEncoding(call.GetBodyData()); + JobAction_Cancel, + JobAction_Pause, + JobAction_Resubmit, + JobAction_Resume + }; + + template <JobAction action> + static void ApplyJobAction(RestApiPostCall& call) + { + std::string id = call.GetUriComponent("id", ""); + + bool ok = false; + + switch (action) + { + case JobAction_Cancel: + ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Cancel(id); + break; - Configuration::SetDefaultEncoding(encoding); + case JobAction_Pause: + ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id); + break; + + case JobAction_Resubmit: + ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resubmit(id); + break; - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + case JobAction_Resume: + ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resume(id); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (ok) + { + call.GetOutput().AnswerBuffer("{}", "application/json"); + } } - + void OrthancRestApi::RegisterSystem() { Register("/", ServeRoot); @@ -284,5 +378,12 @@ Register("/plugins", ListPlugins); Register("/plugins/{id}", GetPlugin); Register("/plugins/explorer.js", GetOrthancExplorerPlugins); + + Register("/jobs", ListJobs); + Register("/jobs/{id}", GetJobInfo); + Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>); + Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>); + Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>); + Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>); } }
--- a/OrthancServer/QueryRetrieveHandler.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/QueryRetrieveHandler.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -35,21 +35,24 @@ #include "QueryRetrieveHandler.h" #include "OrthancInitialization.h" + #include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/Logging.h" namespace Orthanc { - static void FixQuery(DicomMap& query, - ServerContext& context, - const std::string& modality) + static void FixQueryLua(DicomMap& query, + ServerContext& context, + const std::string& modality) { static const char* LUA_CALLBACK = "OutgoingFindRequestFilter"; - LuaScripting::Locker locker(context.GetLua()); - if (locker.GetLua().IsExistingFunction(LUA_CALLBACK)) + LuaScripting::Lock lock(context.GetLuaScripting()); + + if (lock.GetLua().IsExistingFunction(LUA_CALLBACK)) { - LuaFunctionCall call(locker.GetLua(), LUA_CALLBACK); + LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK); call.PushDicom(query); call.PushJson(modality); FromDcmtkBridge::ExecuteToDicom(query, call); @@ -57,8 +60,8 @@ } - static void FixQuery(DicomMap& query, - ModalityManufacturer manufacturer) + static void FixQueryBuiltin(DicomMap& query, + ModalityManufacturer manufacturer) { /** * Introduce patches for specific manufacturers below. @@ -76,6 +79,19 @@ { done_ = false; answers_.Clear(); + connection_.reset(NULL); + } + + + DicomUserConnection& QueryRetrieveHandler::GetConnection() + { + if (connection_.get() == NULL) + { + connection_.reset(new DicomUserConnection(localAet_, modality_)); + connection_->Open(); + } + + return *connection_; } @@ -86,16 +102,12 @@ // Firstly, fix the content of the query for specific manufacturers DicomMap fixed; fixed.Assign(query_); - FixQuery(fixed, modality_.GetManufacturer()); + FixQueryBuiltin(fixed, modality_.GetManufacturer()); // Secondly, possibly fix the query with the user-provider Lua callback - FixQuery(fixed, context_, modality_.GetApplicationEntityTitle()); + FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); - { - // Finally, run the C-FIND SCU against the fixed query - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - locker.GetConnection().Find(answers_, level_, fixed); - } + GetConnection().Find(answers_, level_, fixed); done_ = true; } @@ -155,11 +167,7 @@ { DicomMap map; GetAnswer(map, i); - - { - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - locker.GetConnection().Move(target, map); - } + GetConnection().Move(target, map); }
--- a/OrthancServer/QueryRetrieveHandler.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/QueryRetrieveHandler.h Mon Jun 11 16:30:13 2018 +0200 @@ -49,8 +49,11 @@ DicomFindAnswers answers_; std::string modalityName_; + std::auto_ptr<DicomUserConnection> connection_; + void Invalidate(); + DicomUserConnection& GetConnection(); public: QueryRetrieveHandler(ServerContext& context);
--- a/OrthancServer/Scheduler/CallSystemCommand.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "CallSystemCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/Toolbox.h" -#include "../../Core/TemporaryFile.h" - -namespace Orthanc -{ - CallSystemCommand::CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector<std::string>& arguments) : - context_(context), - command_(command), - arguments_(arguments) - { - } - - bool CallSystemCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Calling system command " << command_ << " on instance " << *it; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - TemporaryFile tmp; - tmp.Write(dicom); - - std::vector<std::string> args = arguments_; - args.push_back(tmp.GetPath()); - - SystemToolbox::ExecuteSystemCommand(command_, args); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to call system command " << command_ - << " on instance " << *it << " in a Lua script: " << e.What(); - } - } - - return true; - } -}
--- a/OrthancServer/Scheduler/CallSystemCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class CallSystemCommand : public IServerCommand - { - private: - ServerContext& context_; - std::string command_; - std::vector<std::string> arguments_; - - public: - CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector<std::string>& arguments); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "DeleteInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Deleting instance " << *it; - - try - { - Json::Value tmp; - context_.DeleteResource(tmp, *it, ResourceType_Instance); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What(); - } - } - - return true; - } -}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class DeleteInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - - public: - DeleteInstanceCommand(ServerContext& context) : context_(context) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -}
--- a/OrthancServer/Scheduler/IServerCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include <list> -#include <string> -#include <boost/noncopyable.hpp> - -namespace Orthanc -{ - class IServerCommand : public boost::noncopyable - { - public: - typedef std::list<std::string> ListOfStrings; - - virtual ~IServerCommand() - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) = 0; - }; -}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "ModifyInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification) : - context_(context), - origin_(origin), - modification_(modification) - { - modification_->SetAllowManualIdentifiers(true); - - if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) - { - modification_->SetLevel(ResourceType_Patient); - } - else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Study); - } - else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Series); - } - else - { - modification_->SetLevel(ResourceType_Instance); - } - - if (origin_ != RequestOrigin_Lua) - { - // TODO If issued from HTTP, "remoteIp" and "username" must be provided - throw OrthancException(ErrorCode_NotImplemented); - } - } - - - ModifyInstanceCommand::~ModifyInstanceCommand() - { - if (modification_) - { - delete modification_; - } - } - - - bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Modifying resource " << *it; - - try - { - std::auto_ptr<ParsedDicomFile> modified; - - { - ServerContext::DicomCacheLocker lock(context_, *it); - modified.reset(lock.GetDicom().Clone(true)); - } - - modification_->Apply(*modified); - - DicomInstanceToStore toStore; - assert(origin_ == RequestOrigin_Lua); - toStore.SetLuaOrigin(); - toStore.SetParsedDicomFile(*modified); - // TODO other metadata - toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); - - std::string modifiedId; - context_.Store(modifiedId, toStore); - - // Only chain with other commands if this command succeeds - outputs.push_back(modifiedId); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What(); - } - } - - return true; - } -}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../../Core/DicomParsing/DicomModification.h" - -namespace Orthanc -{ - class ModifyInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - RequestOrigin origin_; - DicomModification* modification_; - - public: - ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification); // takes the ownership - - virtual ~ModifyInstanceCommand(); - - const DicomModification& GetModification() const - { - return *modification_; - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -}
--- a/OrthancServer/Scheduler/ServerCommandInstance.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "ServerCommandInstance.h" - -#include "../../Core/OrthancException.h" - -namespace Orthanc -{ - bool ServerCommandInstance::Execute(IListener& listener) - { - ListOfStrings outputs; - - bool success = false; - - try - { - if (command_->Apply(outputs, inputs_)) - { - success = true; - } - } - catch (OrthancException&) - { - } - - if (!success) - { - listener.SignalFailure(jobId_); - return true; - } - - for (std::list<ServerCommandInstance*>::iterator - it = next_.begin(); it != next_.end(); ++it) - { - for (ListOfStrings::const_iterator - output = outputs.begin(); output != outputs.end(); ++output) - { - (*it)->AddInput(*output); - } - } - - listener.SignalSuccess(jobId_); - return true; - } - - - ServerCommandInstance::ServerCommandInstance(IServerCommand *command, - const std::string& jobId) : - command_(command), - jobId_(jobId), - connectedToSink_(false) - { - if (command_ == NULL) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - ServerCommandInstance::~ServerCommandInstance() - { - if (command_ != NULL) - { - delete command_; - } - } -}
--- a/OrthancServer/Scheduler/ServerCommandInstance.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "../../Core/IDynamicObject.h" -#include "IServerCommand.h" - -namespace Orthanc -{ - class ServerCommandInstance : public IDynamicObject - { - friend class ServerScheduler; - - public: - class IListener - { - public: - virtual ~IListener() - { - } - - virtual void SignalSuccess(const std::string& jobId) = 0; - - virtual void SignalFailure(const std::string& jobId) = 0; - }; - - private: - typedef IServerCommand::ListOfStrings ListOfStrings; - - IServerCommand *command_; - std::string jobId_; - ListOfStrings inputs_; - std::list<ServerCommandInstance*> next_; - bool connectedToSink_; - - bool Execute(IListener& listener); - - public: - ServerCommandInstance(IServerCommand *command, - const std::string& jobId); - - virtual ~ServerCommandInstance(); - - const std::string& GetJobId() const - { - return jobId_; - } - - void AddInput(const std::string& input) - { - inputs_.push_back(input); - } - - void ConnectOutput(ServerCommandInstance& next) - { - next_.push_back(&next); - } - - void SetConnectedToSink(bool connected = true) - { - connectedToSink_ = connected; - } - - bool IsConnectedToSink() const - { - return connectedToSink_; - } - - const std::list<ServerCommandInstance*>& GetNextCommands() const - { - return next_; - } - }; -}
--- a/OrthancServer/Scheduler/ServerJob.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,147 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "ServerJob.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Toolbox.h" - -namespace Orthanc -{ - void ServerJob::CheckOrdering() - { - std::map<ServerCommandInstance*, unsigned int> index; - - unsigned int count = 0; - for (std::list<ServerCommandInstance*>::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - index[*it] = count++; - } - - for (std::list<ServerCommandInstance*>::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands(); - - for (std::list<ServerCommandInstance*>::const_iterator - next = nextCommands.begin(); next != nextCommands.end(); ++next) - { - if (index.find(*next) == index.end() || - index[*next] <= index[*it]) - { - // You must reorder your calls to "ServerJob::AddCommand" - throw OrthancException(ErrorCode_BadJobOrdering); - } - } - } - } - - - size_t ServerJob::Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener) - { - if (submitted_) - { - // This job has already been submitted - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - CheckOrdering(); - - size_t size = filters_.size(); - - for (std::list<ServerCommandInstance*>::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - target.Enqueue(*it); - } - - filters_.clear(); - submitted_ = true; - - return size; - } - - - ServerJob::ServerJob() : - jobId_(Toolbox::GenerateUuid()), - submitted_(false), - description_("no description") - { - } - - - ServerJob::~ServerJob() - { - for (std::list<ServerCommandInstance*>::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - delete *it; - } - - for (std::list<IDynamicObject*>::iterator - it = payloads_.begin(); it != payloads_.end(); ++it) - { - delete *it; - } - } - - - ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - filters_.push_back(new ServerCommandInstance(filter, jobId_)); - - return *filters_.back(); - } - - - IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - payloads_.push_back(payload); - - return *filters_.back(); - } - -}
--- a/OrthancServer/Scheduler/ServerJob.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "ServerCommandInstance.h" -#include "../../Core/MultiThreading/SharedMessageQueue.h" - -namespace Orthanc -{ - class ServerJob - { - friend class ServerScheduler; - - private: - std::list<ServerCommandInstance*> filters_; - std::list<IDynamicObject*> payloads_; - std::string jobId_; - bool submitted_; - std::string description_; - - void CheckOrdering(); - - size_t Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener); - - public: - ServerJob(); - - ~ServerJob(); - - const std::string& GetId() const - { - return jobId_; - } - - void SetDescription(const std::string& description) - { - description_ = description; - } - - const std::string& GetDescription() const - { - return description_; - } - - ServerCommandInstance& AddCommand(IServerCommand* filter); - - // Take the ownership of a payload to a job. This payload will be - // automatically freed when the job succeeds or fails. - IDynamicObject& AddPayload(IDynamicObject* payload); - }; -}
--- a/OrthancServer/Scheduler/ServerScheduler.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "ServerScheduler.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Logging.h" - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation modules - class Sink : public IServerCommand - { - private: - ListOfStrings& target_; - - public: - explicit Sink(ListOfStrings& target) : target_(target) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - target_.push_back(*it); - } - - return true; - } - }; - } - - - ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) - { - Jobs::iterator info = jobs_.find(jobId); - - if (info == jobs_.end()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return info->second; - } - - - void ServerScheduler::SignalSuccess(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.success_++; - - assert(info.failures_ == 0); - - if (info.success_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Success; - watchedJobFinished_.notify_all(); - } - - LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::SignalFailure(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.failures_++; - - if (info.success_ + info.failures_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Failure; - watchedJobFinished_.notify_all(); - } - - LOG(ERROR) << "Job has failed (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::Worker(ServerScheduler* that) - { - static const int32_t TIMEOUT = 100; - - LOG(WARNING) << "The server scheduler has started"; - - while (!that->finish_) - { - std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT)); - if (object.get() != NULL) - { - ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object); - - // Skip the execution of this filter if its parent job has - // previously failed. - bool jobHasFailed; - { - boost::mutex::scoped_lock lock(that->mutex_); - JobInfo& info = that->GetJobInfo(filter.GetJobId()); - jobHasFailed = (info.failures_ > 0 || info.cancel_); - } - - if (jobHasFailed) - { - that->SignalFailure(filter.GetJobId()); - } - else - { - filter.Execute(*that); - } - } - } - } - - - void ServerScheduler::SubmitInternal(ServerJob& job, - bool watched) - { - availableJob_.Acquire(); - - boost::mutex::scoped_lock lock(mutex_); - - JobInfo info; - info.size_ = job.Submit(queue_, *this); - info.cancel_ = false; - info.success_ = 0; - info.failures_ = 0; - info.description_ = job.GetDescription(); - info.watched_ = watched; - - assert(info.size_ > 0); - - if (watched) - { - watchedJobStatus_[job.GetId()] = JobStatus_Running; - } - - jobs_[job.GetId()] = info; - - LOG(INFO) << "New job submitted (" << job.description_ << ")"; - } - - - ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) - { - if (maxJobs == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - finish_ = false; - worker_ = boost::thread(Worker, this); - } - - - ServerScheduler::~ServerScheduler() - { - if (!finish_) - { - LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Stop(); - } - } - - - void ServerScheduler::Stop() - { - if (!finish_) - { - finish_ = true; - - if (worker_.joinable()) - { - worker_.join(); - } - } - } - - - void ServerScheduler::Submit(ServerJob& job) - { - if (job.filters_.empty()) - { - return; - } - - SubmitInternal(job, false); - } - - - bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, - ServerJob& job) - { - std::string jobId = job.GetId(); - - outputs.clear(); - - if (job.filters_.empty()) - { - return true; - } - - // Add a sink filter to collect all the results of the filters - // that have no next filter. - ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); - - for (std::list<ServerCommandInstance*>::iterator - it = job.filters_.begin(); it != job.filters_.end(); ++it) - { - if ((*it) != &sink && - (*it)->IsConnectedToSink()) - { - (*it)->ConnectOutput(sink); - } - } - - // Submit the job - SubmitInternal(job, true); - - // Wait for the job to complete (either success or failure) - JobStatus status; - - { - boost::mutex::scoped_lock lock(mutex_); - - assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); - - while (watchedJobStatus_[jobId] == JobStatus_Running) - { - watchedJobFinished_.wait(lock); - } - - status = watchedJobStatus_[jobId]; - watchedJobStatus_.erase(jobId); - } - - return (status == JobStatus_Success); - } - - - bool ServerScheduler::SubmitAndWait(ServerJob& job) - { - ListOfStrings ignoredSink; - return SubmitAndWait(ignoredSink, job); - } - - - bool ServerScheduler::IsRunning(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - return jobs_.find(jobId) != jobs_.end(); - } - - - void ServerScheduler::Cancel(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job != jobs_.end()) - { - job->second.cancel_ = true; - LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; - } - } - - - float ServerScheduler::GetProgress(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job == jobs_.end() || - job->second.size_ == 0 /* should never happen */) - { - // This job is not running - return 1; - } - - if (job->second.failures_ != 0) - { - return 1; - } - - if (job->second.size_ == 1) - { - return static_cast<float>(job->second.success_); - } - - return (static_cast<float>(job->second.success_) / - static_cast<float>(job->second.size_ - 1)); - } - - - void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) - { - boost::mutex::scoped_lock lock(mutex_); - - jobs.clear(); - - for (Jobs::const_iterator - it = jobs_.begin(); it != jobs_.end(); ++it) - { - jobs.push_back(it->first); - } - } -}
--- a/OrthancServer/Scheduler/ServerScheduler.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "ServerJob.h" - -#include "../../Core/MultiThreading/Semaphore.h" - -namespace Orthanc -{ - class ServerScheduler : public ServerCommandInstance::IListener - { - private: - struct JobInfo - { - bool watched_; - bool cancel_; - size_t size_; - size_t success_; - size_t failures_; - std::string description_; - }; - - enum JobStatus - { - JobStatus_Running = 1, - JobStatus_Success = 2, - JobStatus_Failure = 3 - }; - - typedef IServerCommand::ListOfStrings ListOfStrings; - typedef std::map<std::string, JobInfo> Jobs; - - boost::mutex mutex_; - boost::condition_variable watchedJobFinished_; - Jobs jobs_; - SharedMessageQueue queue_; - bool finish_; - boost::thread worker_; - std::map<std::string, JobStatus> watchedJobStatus_; - Semaphore availableJob_; - - JobInfo& GetJobInfo(const std::string& jobId); - - virtual void SignalSuccess(const std::string& jobId); - - virtual void SignalFailure(const std::string& jobId); - - static void Worker(ServerScheduler* that); - - void SubmitInternal(ServerJob& job, - bool watched); - - public: - explicit ServerScheduler(unsigned int maxjobs); - - ~ServerScheduler(); - - void Stop(); - - void Submit(ServerJob& job); - - bool SubmitAndWait(ListOfStrings& outputs, - ServerJob& job); - - bool SubmitAndWait(ServerJob& job); - - bool IsRunning(const std::string& jobId); - - void Cancel(const std::string& jobId); - - // Returns a number between 0 and 1 - float GetProgress(const std::string& jobId); - - bool IsRunning(const ServerJob& job) - { - return IsRunning(job.GetId()); - } - - void Cancel(const ServerJob& job) - { - Cancel(job.GetId()); - } - - float GetProgress(const ServerJob& job) - { - return GetProgress(job.GetId()); - } - - void GetListOfJobs(ListOfStrings& jobs); - }; -}
--- a/OrthancServer/Scheduler/StorePeerCommand.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "StorePeerCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/HttpClient.h" - -namespace Orthanc -{ - StorePeerCommand::StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions) : - context_(context), - peer_(peer), - ignoreExceptions_(ignoreExceptions) - { - } - - bool StorePeerCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - // Configure the HTTP client - HttpClient client(peer_, "instances"); - client.SetMethod(HttpMethod_Post); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to peer \"" - << peer_.GetUrl() << "\""; - - try - { - context_.ReadDicom(client.GetBody(), *it); - - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; - throw OrthancException(ErrorCode_NetworkProtocol); - } - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance " - << *it << ", peer " << peer_.GetUrl() << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -}
--- a/OrthancServer/Scheduler/StorePeerCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../OrthancInitialization.h" - -namespace Orthanc -{ - class StorePeerCommand : public IServerCommand - { - private: - ServerContext& context_; - WebServiceParameters peer_; - bool ignoreExceptions_; - - public: - StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -}
--- a/OrthancServer/Scheduler/StoreScuCommand.cpp Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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" -#include "StoreScuCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - StoreScuCommand::StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions) : - context_(context), - modality_(modality), - ignoreExceptions_(ignoreExceptions), - localAet_(localAet), - moveOriginatorID_(0) - { - } - - - void StoreScuCommand::SetMoveOriginator(const std::string& aet, - uint16_t id) - { - moveOriginatorAET_ = aet; - moveOriginatorID_ = id; - } - - - bool StoreScuCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to modality \"" - << modality_.GetApplicationEntityTitle() << "\""; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - // Ignore transmission errors (e.g. if the remote modality is - // powered off) - LOG(ERROR) << "Unable to forward to a modality in (instance " - << *it << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -}
--- a/OrthancServer/Scheduler/StoreScuCommand.h Thu Jun 07 17:15:55 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., 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. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * 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/>. - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class StoreScuCommand : public IServerCommand - { - private: - ServerContext& context_; - RemoteModalityParameters modality_; - bool ignoreExceptions_; - std::string localAet_; - std::string moveOriginatorAET_; - uint16_t moveOriginatorID_; - - public: - StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions); - - void SetMoveOriginator(const std::string& aet, - uint16_t id); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -}
--- a/OrthancServer/ServerContext.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/ServerContext.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,27 +34,22 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/FileStorage/StorageAccessor.h" #include "../Core/HttpServer/FilesystemHttpSender.h" #include "../Core/HttpServer/HttpStreamTranscoder.h" #include "../Core/Logging.h" -#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Plugins/Engine/OrthancPlugins.h" +#include "OrthancInitialization.h" +#include "OrthancRestApi/OrthancRestApi.h" +#include "Search/LookupResource.h" +#include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" -#include "OrthancInitialization.h" #include <EmbeddedResources.h> #include <dcmtk/dcmdata/dcfilefo.h> -#include "Scheduler/CallSystemCommand.h" -#include "Scheduler/DeleteInstanceCommand.h" -#include "Scheduler/ModifyInstanceCommand.h" -#include "Scheduler/StoreScuCommand.h" -#include "Scheduler/StorePeerCommand.h" -#include "OrthancRestApi/OrthancRestApi.h" -#include "../Plugins/Engine/OrthancPlugins.h" -#include "Search/LookupResource.h" - #define ENABLE_DICOM_CACHE 1 @@ -71,11 +66,12 @@ namespace Orthanc { - void ServerContext::ChangeThread(ServerContext* that) + void ServerContext::ChangeThread(ServerContext* that, + unsigned int sleepDelay) { while (!that->done_) { - std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(100)); + std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay)); if (obj.get() != NULL) { @@ -112,29 +108,141 @@ } + void ServerContext::SaveJobsThread(ServerContext* that, + unsigned int sleepDelay) + { + static const boost::posix_time::time_duration PERIODICITY = + boost::posix_time::seconds(10); + + boost::posix_time::ptime next = + boost::posix_time::microsec_clock::universal_time() + PERIODICITY; + + while (!that->done_) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay)); + + if (that->haveJobsChanged_ || + boost::posix_time::microsec_clock::universal_time() >= next) + { + that->haveJobsChanged_ = false; + that->SaveJobsEngine(); + next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY; + } + } + } + + + void ServerContext::SignalJobSubmitted(const std::string& jobId) + { + haveJobsChanged_ = true; + + // TODO: Call Lua + } + + + void ServerContext::SignalJobSuccess(const std::string& jobId) + { + haveJobsChanged_ = true; + + // TODO: Call Lua + } + + + void ServerContext::SignalJobFailure(const std::string& jobId) + { + haveJobsChanged_ = true; + + // TODO: Call Lua + } + + + void ServerContext::SetupJobsEngine(bool unitTesting, + bool loadJobsFromDatabase) + { + jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2)); + jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); + + if (loadJobsFromDatabase) + { + std::string serialized; + if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry)) + { + LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc"; + OrthancJobUnserializer unserializer(*this); + + try + { + jobsEngine_.LoadRegistryFromString(unserializer, serialized); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot unserialize the jobs engine: " << e.What(); + throw; + } + } + else + { + LOG(INFO) << "The last execution of Orthanc has archived no job"; + } + } + else + { + LOG(WARNING) << "Not reloading the jobs from the last execution of Orthanc"; + } + + //jobsEngine_.GetRegistry().SetMaxCompleted // TODO + + jobsEngine_.GetRegistry().SetObserver(*this); + jobsEngine_.Start(); + } + + + void ServerContext::SaveJobsEngine() + { + LOG(INFO) << "Serializing the content of the jobs engine"; + + try + { + Json::Value value; + jobsEngine_.GetRegistry().Serialize(value); + + Json::FastWriter writer; + std::string serialized = writer.write(value); + + index_.SetGlobalProperty(GlobalProperty_JobsRegistry, serialized); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot serialize the jobs engine: " << e.What(); + } + } + + ServerContext::ServerContext(IDatabaseWrapper& database, - IStorageArea& area) : - index_(*this, database), + IStorageArea& area, + bool unitTesting, + bool loadJobsFromDatabase) : + index_(*this, database, (unitTesting ? 20 : 500)), area_(area), compressionEnabled_(false), storeMD5_(true), provider_(*this), dicomCache_(provider_, DICOM_CACHE_SIZE), - scheduler_(Configuration::GetGlobalUnsignedIntegerParameter("LimitJobs", 10)), lua_(*this), #if ORTHANC_ENABLE_PLUGINS == 1 plugins_(NULL), #endif done_(false), + haveJobsChanged_(false), queryRetrieveArchive_(Configuration::GetGlobalUnsignedIntegerParameter("QueryRetrieveSize", 10)), defaultLocalAet_(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")) { - uint64_t s = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); // In seconds - scu_.SetMillisecondsBeforeClose(s * 1000); // Milliseconds are expected here - listeners_.push_back(ServerListener(lua_, "Lua")); - changeThread_ = boost::thread(ChangeThread, this); + SetupJobsEngine(unitTesting, loadJobsFromDatabase); + + changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100)); } @@ -165,10 +273,16 @@ changeThread_.join(); } - scu_.Finalize(); + if (saveJobsThread_.joinable()) + { + saveJobsThread_.join(); + } + + jobsEngine_.GetRegistry().ResetObserver(); + SaveJobsEngine(); // Do not change the order below! - scheduler_.Stop(); + jobsEngine_.Stop(); index_.Stop(); } } @@ -679,4 +793,19 @@ } } + + void ServerContext::AddChildInstances(SetOfInstancesJob& job, + const std::string& publicId) + { + std::list<std::string> instances; + GetIndex().GetChildInstances(instances, publicId); + + job.Reserve(job.GetInstancesCount() + instances.size()); + + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + job.AddInstance(*it); + } + } }
--- a/OrthancServer/ServerContext.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/ServerContext.h Mon Jun 11 16:30:13 2018 +0200 @@ -33,21 +33,21 @@ #pragma once -#include "../Core/MultiThreading/SharedMessageQueue.h" +#include "DicomInstanceToStore.h" +#include "IServerListener.h" +#include "LuaScripting.h" +#include "OrthancHttpHandler.h" +#include "ServerIndex.h" + #include "../Core/Cache/MemoryCache.h" #include "../Core/Cache/SharedArchive.h" +#include "../Core/DicomParsing/ParsedDicomFile.h" #include "../Core/FileStorage/IStorageArea.h" -#include "../Core/Lua/LuaContext.h" +#include "../Core/JobsEngine/JobsEngine.h" +#include "../Core/JobsEngine/SetOfInstancesJob.h" +#include "../Core/MultiThreading/SharedMessageQueue.h" #include "../Core/RestApi/RestApiOutput.h" #include "../Plugins/Engine/OrthancPlugins.h" -#include "DicomInstanceToStore.h" -#include "../Core/DicomNetworking/ReusableDicomUserConnection.h" -#include "IServerListener.h" -#include "LuaScripting.h" -#include "../Core/DicomParsing/ParsedDicomFile.h" -#include "Scheduler/ServerScheduler.h" -#include "ServerIndex.h" -#include "OrthancHttpHandler.h" #include <boost/filesystem.hpp> #include <boost/thread.hpp> @@ -60,7 +60,7 @@ * filesystem (including compression), as well as the index of the * DICOM store. It implements the required locking mechanisms. **/ - class ServerContext + class ServerContext : private JobsRegistry::IObserver { private: class DicomCacheProvider : public ICachePageProvider @@ -104,11 +104,26 @@ typedef std::list<ServerListener> ServerListeners; - static void ChangeThread(ServerContext* that); + static void ChangeThread(ServerContext* that, + unsigned int sleepDelay); + + static void SaveJobsThread(ServerContext* that, + unsigned int sleepDelay); void ReadDicomAsJsonInternal(std::string& result, const std::string& instancePublicId); + void SetupJobsEngine(bool unitTesting, + bool loadJobsFromDatabase); + + void SaveJobsEngine(); + + virtual void SignalJobSubmitted(const std::string& jobId); + + virtual void SignalJobSuccess(const std::string& jobId); + + virtual void SignalJobFailure(const std::string& jobId); + ServerIndex index_; IStorageArea& area_; @@ -118,8 +133,7 @@ DicomCacheProvider provider_; boost::mutex dicomCacheMutex_; MemoryCache dicomCache_; - ReusableDicomUserConnection scu_; - ServerScheduler scheduler_; + JobsEngine jobsEngine_; LuaScripting lua_; @@ -131,8 +145,10 @@ boost::recursive_mutex listenersMutex_; bool done_; + bool haveJobsChanged_; SharedMessageQueue pendingChanges_; boost::thread changeThread_; + boost::thread saveJobsThread_; SharedArchive queryRetrieveArchive_; std::string defaultLocalAet_; @@ -159,7 +175,9 @@ }; ServerContext(IDatabaseWrapper& database, - IStorageArea& area); + IStorageArea& area, + bool unitTesting, + bool loadJobsFromDatabase); ~ServerContext(); @@ -238,14 +256,9 @@ return storeMD5_; } - ReusableDicomUserConnection& GetReusableDicomUserConnection() + JobsEngine& GetJobsEngine() { - return scu_; - } - - ServerScheduler& GetScheduler() - { - return scheduler_; + return jobsEngine_; } bool DeleteResource(Json::Value& target, @@ -264,7 +277,7 @@ return defaultLocalAet_; } - LuaScripting& GetLua() + LuaScripting& GetLuaScripting() { return lua_; } @@ -297,5 +310,8 @@ #endif bool HasPlugins() const; + + void AddChildInstances(SetOfInstancesJob& job, + const std::string& publicId); }; }
--- a/OrthancServer/ServerEnumerations.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/ServerEnumerations.h Mon Jun 11 16:30:13 2018 +0200 @@ -76,7 +76,8 @@ GlobalProperty_DatabaseSchemaVersion = 1, // Unused in the Orthanc core as of Orthanc 0.9.5 GlobalProperty_FlushSleep = 2, GlobalProperty_AnonymizationSequence = 3, - GlobalProperty_DatabasePatchLevel = 4 // Reserved for internal use of the database plugins + GlobalProperty_DatabasePatchLevel = 4, // Reserved for internal use of the database plugins + GlobalProperty_JobsRegistry = 5 }; enum MetadataType
--- a/OrthancServer/ServerIndex.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/ServerIndex.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -342,7 +342,8 @@ } - void ServerIndex::FlushThread(ServerIndex* that) + void ServerIndex::FlushThread(ServerIndex* that, + unsigned int threadSleep) { // By default, wait for 10 seconds before flushing unsigned int sleep = 10; @@ -368,7 +369,7 @@ while (!that->done_) { - boost::this_thread::sleep(boost::posix_time::seconds(1)); + boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); count++; if (count < sleep) { @@ -538,7 +539,8 @@ ServerIndex::ServerIndex(ServerContext& context, - IDatabaseWrapper& db) : + IDatabaseWrapper& db, + unsigned int threadSleep) : done_(false), db_(db), maximumStorageSize_(0), @@ -555,10 +557,11 @@ if (db.HasFlushToDisk()) { - flushThread_ = boost::thread(FlushThread, this); + flushThread_ = boost::thread(FlushThread, this, threadSleep); } - unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this); + unstableResourcesMonitorThread_ = boost::thread + (UnstableResourcesMonitorThread, this, threadSleep); } @@ -778,9 +781,10 @@ // Attach the auto-computed metadata for the instance level, // reflecting these additions into the input metadata map SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now); - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, instanceToStore.GetRemoteAet()); + SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, + instanceToStore.GetOrigin().GetRemoteAetC()); SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, - EnumerationToString(instanceToStore.GetRequestOrigin())); + EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); { std::string s; @@ -1212,7 +1216,7 @@ ResourceType type; if (!db_.LookupResource(id, type, publicId)) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InexistentItem); } std::string patientId; @@ -1877,7 +1881,8 @@ } - void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that) + void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, + unsigned int threadSleep) { int stableAge = Configuration::GetGlobalUnsignedIntegerParameter("StableAge", 60); if (stableAge <= 0) @@ -1889,8 +1894,8 @@ while (!that->done_) { - // Check for stable resources each second - boost::this_thread::sleep(boost::posix_time::seconds(1)); + // Check for stable resources each few seconds + boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); boost::mutex::scoped_lock lock(that->mutex_); @@ -2091,13 +2096,20 @@ } + bool ServerIndex::LookupGlobalProperty(std::string& value, + GlobalProperty property) + { + boost::mutex::scoped_lock lock(mutex_); + return db_.LookupGlobalProperty(value, property); + } + + std::string ServerIndex::GetGlobalProperty(GlobalProperty property, const std::string& defaultValue) { - boost::mutex::scoped_lock lock(mutex_); - std::string value; - if (db_.LookupGlobalProperty(value, property)) + + if (LookupGlobalProperty(value, property)) { return value; }
--- a/OrthancServer/ServerIndex.h Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/ServerIndex.h Mon Jun 11 16:30:13 2018 +0200 @@ -74,9 +74,11 @@ uint64_t maximumStorageSize_; unsigned int maximumPatients_; - static void FlushThread(ServerIndex* that); + static void FlushThread(ServerIndex* that, + unsigned int threadSleep); - static void UnstableResourcesMonitorThread(ServerIndex* that); + static void UnstableResourcesMonitorThread(ServerIndex* that, + unsigned int threadSleep); void MainDicomTagsToJson(Json::Value& result, int64_t resourceId, @@ -124,7 +126,8 @@ public: ServerIndex(ServerContext& context, - IDatabaseWrapper& database); + IDatabaseWrapper& database, + unsigned int threadSleep); ~ServerIndex(); @@ -255,6 +258,9 @@ void SetGlobalProperty(GlobalProperty property, const std::string& value); + bool LookupGlobalProperty(std::string& value, + GlobalProperty property); + std::string GetGlobalProperty(GlobalProperty property, const std::string& defaultValue);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,904 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ArchiveJob.h" + +#include "../../Core/Compression/HierarchicalZipWriter.h" +#include "../../Core/DicomParsing/DicomDirWriter.h" +#include "../../Core/Logging.h" +#include "../../Core/OrthancException.h" + +#include <stdio.h> + +#if defined(_MSC_VER) +#define snprintf _snprintf +#endif + +static const uint64_t MEGA_BYTES = 1024 * 1024; +static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024; +static const char* MEDIA_IMAGES_FOLDER = "IMAGES"; + +namespace Orthanc +{ + static bool IsZip64Required(uint64_t uncompressedSize, + unsigned int countInstances) + { + static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; // Should be large enough to hold DICOMDIR + static const unsigned int FILES_MARGIN = 10; + + /** + * Determine whether ZIP64 is required. Original ZIP format can + * store up to 2GB of data (some implementation supporting up to + * 4GB of data), and up to 65535 files. + * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64 + **/ + + const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN || + countInstances >= 65535 - FILES_MARGIN); + + LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size " + << (uncompressedSize / MEGA_BYTES) << "MB using the " + << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; + + return isZip64; + } + + + class ArchiveJob::ResourceIdentifiers : public boost::noncopyable + { + private: + ResourceType level_; + std::string patient_; + std::string study_; + std::string series_; + std::string instance_; + + static void GoToParent(ServerIndex& index, + std::string& current) + { + std::string tmp; + + if (index.LookupParent(tmp, current)) + { + current = tmp; + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + public: + ResourceIdentifiers(ServerIndex& index, + const std::string& publicId) + { + if (!index.LookupResourceType(level_, publicId)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + std::string current = publicId;; + switch (level_) // Do not add "break" below! + { + case ResourceType_Instance: + instance_ = current; + GoToParent(index, current); + + case ResourceType_Series: + series_ = current; + GoToParent(index, current); + + case ResourceType_Study: + study_ = current; + GoToParent(index, current); + + case ResourceType_Patient: + patient_ = current; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + ResourceType GetLevel() const + { + return level_; + } + + const std::string& GetIdentifier(ResourceType level) const + { + // Some sanity check to ensure enumerations are not altered + assert(ResourceType_Patient < ResourceType_Study); + assert(ResourceType_Study < ResourceType_Series); + assert(ResourceType_Series < ResourceType_Instance); + + if (level > level_) + { + throw OrthancException(ErrorCode_InternalError); + } + + switch (level) + { + case ResourceType_Patient: + return patient_; + + case ResourceType_Study: + return study_; + + case ResourceType_Series: + return series_; + + case ResourceType_Instance: + return instance_; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + }; + + + class ArchiveJob::IArchiveVisitor : public boost::noncopyable + { + public: + virtual ~IArchiveVisitor() + { + } + + virtual void Open(ResourceType level, + const std::string& publicId) = 0; + + virtual void Close() = 0; + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) = 0; + }; + + + class ArchiveJob::ArchiveIndex : public boost::noncopyable + { + private: + struct Instance + { + std::string id_; + FileInfo dicom_; + + Instance(const std::string& id, + const FileInfo& dicom) : + id_(id), dicom_(dicom) + { + } + }; + + // A "NULL" value for ArchiveIndex indicates a non-expanded node + typedef std::map<std::string, ArchiveIndex*> Resources; + + ResourceType level_; + Resources resources_; // Only at patient/study/series level + std::list<Instance> instances_; // Only at instance level + + + void AddResourceToExpand(ServerIndex& index, + const std::string& id) + { + if (level_ == ResourceType_Instance) + { + FileInfo tmp; + if (index.LookupAttachment(tmp, id, FileContentType_Dicom)) + { + instances_.push_back(Instance(id, tmp)); + } + } + else + { + resources_[id] = NULL; + } + } + + + public: + ArchiveIndex(ResourceType level) : + level_(level) + { + } + + ~ArchiveIndex() + { + for (Resources::iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + delete it->second; + } + } + + + void Add(ServerIndex& index, + const ResourceIdentifiers& resource) + { + const std::string& id = resource.GetIdentifier(level_); + Resources::iterator previous = resources_.find(id); + + if (level_ == ResourceType_Instance) + { + AddResourceToExpand(index, id); + } + else if (resource.GetLevel() == level_) + { + // Mark this resource for further expansion + if (previous != resources_.end()) + { + delete previous->second; + } + + resources_[id] = NULL; + } + else if (previous == resources_.end()) + { + // This is the first time we meet this resource + std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_))); + child->Add(index, resource); + resources_[id] = child.release(); + } + else if (previous->second != NULL) + { + previous->second->Add(index, resource); + } + else + { + // Nothing to do: This item is marked for further expansion + } + } + + + void Expand(ServerIndex& index) + { + if (level_ == ResourceType_Instance) + { + // Expanding an instance node makes no sense + return; + } + + for (Resources::iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + if (it->second == NULL) + { + // This is resource is marked for expansion + std::list<std::string> children; + index.GetChildren(children, it->first); + + std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_))); + + for (std::list<std::string>::const_iterator + it2 = children.begin(); it2 != children.end(); ++it2) + { + child->AddResourceToExpand(index, *it2); + } + + it->second = child.release(); + } + + assert(it->second != NULL); + it->second->Expand(index); + } + } + + + void Apply(IArchiveVisitor& visitor) const + { + if (level_ == ResourceType_Instance) + { + for (std::list<Instance>::const_iterator + it = instances_.begin(); it != instances_.end(); ++it) + { + visitor.AddInstance(it->id_, it->dicom_); + } + } + else + { + for (Resources::const_iterator it = resources_.begin(); + it != resources_.end(); ++it) + { + assert(it->second != NULL); // There must have been a call to "Expand()" + visitor.Open(level_, it->first); + it->second->Apply(visitor); + visitor.Close(); + } + } + } + }; + + + + class ArchiveJob::ZipCommands : public boost::noncopyable + { + private: + enum Type + { + Type_OpenDirectory, + Type_CloseDirectory, + Type_WriteInstance + }; + + class Command : public boost::noncopyable + { + private: + Type type_; + std::string filename_; + std::string instanceId_; + FileInfo info_; + + public: + explicit Command(Type type) : + type_(type) + { + assert(type_ == Type_CloseDirectory); + } + + Command(Type type, + const std::string& filename) : + type_(type), + filename_(filename) + { + assert(type_ == Type_OpenDirectory); + } + + Command(Type type, + const std::string& filename, + const std::string& instanceId, + const FileInfo& info) : + type_(type), + filename_(filename), + instanceId_(instanceId), + info_(info) + { + assert(type_ == Type_WriteInstance); + } + + void Apply(HierarchicalZipWriter& writer, + ServerContext& context, + DicomDirWriter* dicomDir, + const std::string& dicomDirFolder) const + { + switch (type_) + { + case Type_OpenDirectory: + writer.OpenDirectory(filename_.c_str()); + break; + + case Type_CloseDirectory: + writer.CloseDirectory(); + break; + + case Type_WriteInstance: + { + std::string content; + + try + { + context.ReadAttachment(content, info_); + } + catch (OrthancException& e) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_; + return; + } + + //boost::this_thread::sleep(boost::posix_time::milliseconds(300)); + + writer.OpenFile(filename_.c_str()); + writer.Write(content); + + if (dicomDir != NULL) + { + ParsedDicomFile parsed(content); + dicomDir->Add(dicomDirFolder, filename_, parsed); + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + }; + + std::deque<Command*> commands_; + uint64_t uncompressedSize_; + unsigned int instancesCount_; + + + void ApplyInternal(HierarchicalZipWriter& writer, + ServerContext& context, + size_t index, + DicomDirWriter* dicomDir, + const std::string& dicomDirFolder) const + { + if (index >= commands_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder); + } + + public: + ZipCommands() : + uncompressedSize_(0), + instancesCount_(0) + { + } + + ~ZipCommands() + { + for (std::deque<Command*>::iterator it = commands_.begin(); + it != commands_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + size_t GetSize() const + { + return commands_.size(); + } + + unsigned int GetInstancesCount() const + { + return instancesCount_; + } + + uint64_t GetUncompressedSize() const + { + return uncompressedSize_; + } + + void Apply(HierarchicalZipWriter& writer, + ServerContext& context, + size_t index, + DicomDirWriter& dicomDir, + const std::string& dicomDirFolder) const + { + ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder); + } + + void Apply(HierarchicalZipWriter& writer, + ServerContext& context, + size_t index) const + { + ApplyInternal(writer, context, index, NULL, ""); + } + + void AddOpenDirectory(const std::string& filename) + { + commands_.push_back(new Command(Type_OpenDirectory, filename)); + } + + void AddCloseDirectory() + { + commands_.push_back(new Command(Type_CloseDirectory)); + } + + void AddWriteInstance(const std::string& filename, + const std::string& instanceId, + const FileInfo& info) + { + commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info)); + instancesCount_ ++; + uncompressedSize_ += info.GetUncompressedSize(); + } + + bool IsZip64() const + { + return IsZip64Required(GetUncompressedSize(), GetInstancesCount()); + } + }; + + + + class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor + { + private: + ZipCommands& commands_; + ServerContext& context_; + char instanceFormat_[24]; + unsigned int counter_; + + static std::string GetTag(const DicomMap& tags, + const DicomTag& tag) + { + const DicomValue* v = tags.TestAndGetValue(tag); + if (v != NULL && + !v->IsBinary() && + !v->IsNull()) + { + return v->GetContent(); + } + else + { + return ""; + } + } + + public: + ArchiveIndexVisitor(ZipCommands& commands, + ServerContext& context) : + commands_(commands), + context_(context), + counter_(0) + { + if (commands.GetSize() != 0) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); + } + + virtual void Open(ResourceType level, + const std::string& publicId) + { + std::string path; + + DicomMap tags; + if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level)) + { + switch (level) + { + case ResourceType_Patient: + path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME); + break; + + case ResourceType_Study: + path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION); + break; + + case ResourceType_Series: + { + std::string modality = GetTag(tags, DICOM_TAG_MODALITY); + path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION); + + if (modality.size() == 0) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); + } + else if (modality.size() == 1) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", + toupper(modality[0])); + } + else if (modality.size() >= 2) + { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", + toupper(modality[0]), toupper(modality[1])); + } + + counter_ = 0; + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path)); + + if (path.empty()) + { + path = std::string("Unknown ") + EnumerationToString(level); + } + + commands_.AddOpenDirectory(path.c_str()); + } + + virtual void Close() + { + commands_.AddCloseDirectory(); + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) + { + char filename[24]; + snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_); + counter_ ++; + + commands_.AddWriteInstance(filename, instanceId, dicom); + } + }; + + + class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor + { + private: + ZipCommands& commands_; + ServerContext& context_; + unsigned int counter_; + + public: + MediaIndexVisitor(ZipCommands& commands, + ServerContext& context) : + commands_(commands), + context_(context), + counter_(0) + { + } + + virtual void Open(ResourceType level, + const std::string& publicId) + { + } + + virtual void Close() + { + } + + virtual void AddInstance(const std::string& instanceId, + const FileInfo& dicom) + { + // "DICOM restricts the filenames on DICOM media to 8 + // characters (some systems wrongly use 8.3, but this does not + // conform to the standard)." + std::string filename = "IM" + boost::lexical_cast<std::string>(counter_); + commands_.AddWriteInstance(filename, instanceId, dicom); + + counter_ ++; + } + }; + + + class ArchiveJob::ZipWriterIterator : public boost::noncopyable + { + private: + TemporaryFile& target_; + ServerContext& context_; + ZipCommands commands_; + std::auto_ptr<HierarchicalZipWriter> zip_; + std::auto_ptr<DicomDirWriter> dicomDir_; + bool isMedia_; + + public: + ZipWriterIterator(TemporaryFile& target, + ServerContext& context, + ArchiveIndex& archive, + bool isMedia, + bool enableExtendedSopClass) : + target_(target), + context_(context), + isMedia_(isMedia) + { + if (isMedia) + { + MediaIndexVisitor visitor(commands_, context); + archive.Expand(context.GetIndex()); + + commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER); + archive.Apply(visitor); + commands_.AddCloseDirectory(); + + dicomDir_.reset(new DicomDirWriter); + dicomDir_->EnableExtendedSopClass(enableExtendedSopClass); + } + else + { + ArchiveIndexVisitor visitor(commands_, context); + archive.Expand(context.GetIndex()); + archive.Apply(visitor); + } + + zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str())); + zip_->SetZip64(commands_.IsZip64()); + } + + size_t GetStepsCount() const + { + return commands_.GetSize() + 1; + } + + void RunStep(size_t index) + { + if (index > commands_.GetSize()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else if (index == commands_.GetSize()) + { + // Last step: Add the DICOMDIR + if (isMedia_) + { + assert(dicomDir_.get() != NULL); + std::string s; + dicomDir_->Encode(s); + + zip_->OpenFile("DICOMDIR"); + zip_->Write(s); + } + } + else + { + if (isMedia_) + { + assert(dicomDir_.get() != NULL); + commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER); + } + else + { + assert(dicomDir_.get() == NULL); + commands_.Apply(*zip_, context_, index); + } + } + } + + unsigned int GetInstancesCount() const + { + return commands_.GetInstancesCount(); + } + + uint64_t GetUncompressedSize() const + { + return commands_.GetUncompressedSize(); + } + }; + + + ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target, + ServerContext& context, + bool isMedia, + bool enableExtendedSopClass) : + target_(target), + context_(context), + archive_(new ArchiveIndex(ResourceType_Patient)), // root + isMedia_(isMedia), + enableExtendedSopClass_(enableExtendedSopClass), + currentStep_(0), + instancesCount_(0), + uncompressedSize_(0) + { + if (target.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + + void ArchiveJob::AddResource(const std::string& publicId) + { + if (writer_.get() != NULL) // Already started + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + ResourceIdentifiers resource(context_.GetIndex(), publicId); + archive_->Add(context_.GetIndex(), resource); + } + + + void ArchiveJob::SignalResubmit() + { + LOG(ERROR) << "Cannot resubmit the creation of an archive"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + + void ArchiveJob::Start() + { + if (writer_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + writer_.reset(new ZipWriterIterator(*target_, context_, *archive_, + isMedia_, enableExtendedSopClass_)); + + instancesCount_ = writer_->GetInstancesCount(); + uncompressedSize_ = writer_->GetUncompressedSize(); + } + + + JobStepResult ArchiveJob::ExecuteStep() + { + assert(writer_.get() != NULL); + + if (target_.unique()) + { + LOG(WARNING) << "A client has disconnected while creating an archive"; + return JobStepResult::Failure(ErrorCode_NetworkProtocol); + } + + if (writer_->GetStepsCount() == 0) + { + writer_.reset(NULL); // Flush all the results + return JobStepResult::Success(); + } + else + { + writer_->RunStep(currentStep_); + + currentStep_ ++; + + if (currentStep_ == writer_->GetStepsCount()) + { + writer_.reset(NULL); // Flush all the results + return JobStepResult::Success(); + } + else + { + return JobStepResult::Continue(); + } + } + } + + + float ArchiveJob::GetProgress() + { + if (writer_.get() == NULL || + writer_->GetStepsCount() == 0) + { + return 1; + } + else + { + return (static_cast<float>(currentStep_) / + static_cast<float>(writer_->GetStepsCount() - 1)); + } + } + + + void ArchiveJob::GetJobType(std::string& target) + { + if (isMedia_) + { + target = "Media"; + } + else + { + target = "Archive"; + } + } + + + void ArchiveJob::GetPublicContent(Json::Value& value) + { + value["Description"] = description_; + value["InstancesCount"] = instancesCount_; + value["UncompressedSizeMB"] = + static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,105 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/JobsEngine/IJob.h" +#include "../../Core/TemporaryFile.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class ArchiveJob : public IJob + { + private: + class ArchiveIndex; + class ArchiveIndexVisitor; + class IArchiveVisitor; + class MediaIndexVisitor; + class ResourceIdentifiers; + class ZipCommands; + class ZipWriterIterator; + class ZipWriterIterator; + + boost::shared_ptr<TemporaryFile> target_; + ServerContext& context_; + std::auto_ptr<ArchiveIndex> archive_; + bool isMedia_; + bool enableExtendedSopClass_; + std::string description_; + + std::auto_ptr<ZipWriterIterator> writer_; + size_t currentStep_; + unsigned int instancesCount_; + uint64_t uncompressedSize_; + + public: + ArchiveJob(boost::shared_ptr<TemporaryFile>& target, + ServerContext& context, + bool isMedia, + bool enableExtendedSopClass); + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + void AddResource(const std::string& publicId); + + virtual void SignalResubmit(); + + virtual void Start(); + + virtual JobStepResult ExecuteStep(); + + virtual void ReleaseResources() + { + } + + virtual float GetProgress(); + + virtual void GetJobType(std::string& target); + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& value) + { + return false; // Cannot serialize this kind of job + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,222 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "DicomModalityStoreJob.h" + +#include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" + +namespace Orthanc +{ + void DicomModalityStoreJob::OpenConnection() + { + if (connection_.get() == NULL) + { + connection_.reset(new DicomUserConnection); + connection_->SetLocalApplicationEntityTitle(localAet_); + connection_->SetRemoteModality(remote_); + } + } + + + bool DicomModalityStoreJob::HandleInstance(const std::string& instance) + { + assert(IsStarted()); + OpenConnection(); + + LOG(INFO) << "Sending instance " << instance << " to modality \"" + << remote_.GetApplicationEntityTitle() << "\""; + + std::string dicom; + + try + { + context_.ReadDicom(dicom, instance); + } + catch (OrthancException& e) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instance; + return false; + } + + if (HasMoveOriginator()) + { + connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_); + } + else + { + connection_->Store(dicom); + } + + //boost::this_thread::sleep(boost::posix_time::milliseconds(500)); + + return true; + } + + + DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : + context_(context), + localAet_("ORTHANC"), + moveOriginatorId_(0) // By default, not a C-MOVE + { + } + + + void DicomModalityStoreJob::SetLocalAet(const std::string& aet) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + localAet_ = aet; + } + } + + + void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + remote_ = remote; + } + } + + + const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const + { + if (HasMoveOriginator()) + { + return moveOriginatorAet_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + uint16_t DicomModalityStoreJob::GetMoveOriginatorId() const + { + if (HasMoveOriginator()) + { + return moveOriginatorId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomModalityStoreJob::SetMoveOriginator(const std::string& aet, + int id) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (id < 0 || + id >= 65536) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + moveOriginatorId_ = static_cast<uint16_t>(id); + moveOriginatorAet_ = aet; + } + } + + void DicomModalityStoreJob::ReleaseResources() // For pausing jobs + { + connection_.reset(NULL); + } + + + void DicomModalityStoreJob::GetPublicContent(Json::Value& value) + { + SetOfInstancesJob::GetPublicContent(value); + + value["LocalAet"] = localAet_; + value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + + if (HasMoveOriginator()) + { + value["MoveOriginatorAET"] = GetMoveOriginatorAet(); + value["MoveOriginatorID"] = GetMoveOriginatorId(); + } + } + + + static const char* LOCAL_AET = "LocalAet"; + static const char* REMOTE = "Remote"; + static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet"; + static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId"; + + + DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), + context_(context) + { + localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); + remote_ = RemoteModalityParameters(serialized[REMOTE]); + moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); + moveOriginatorId_ = static_cast<uint16_t> + (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID)); + } + + + bool DicomModalityStoreJob::Serialize(Json::Value& target) + { + if (!SetOfInstancesJob::Serialize(target)) + { + return false; + } + else + { + target[LOCAL_AET] = localAet_; + remote_.Serialize(target[REMOTE]); + target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_; + target[MOVE_ORIGINATOR_ID] = moveOriginatorId_; + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,101 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" +#include "../../Core/DicomNetworking/DicomUserConnection.h" + +#include "../ServerContext.h" + +namespace Orthanc +{ + class DicomModalityStoreJob : public SetOfInstancesJob + { + private: + ServerContext& context_; + std::string localAet_; + RemoteModalityParameters remote_; + std::string moveOriginatorAet_; + uint16_t moveOriginatorId_; + std::auto_ptr<DicomUserConnection> connection_; + + void OpenConnection(); + + protected: + virtual bool HandleInstance(const std::string& instance); + + public: + DicomModalityStoreJob(ServerContext& context); + + DicomModalityStoreJob(ServerContext& context, + const Json::Value& serialized); + + const std::string& GetLocalAet() const + { + return localAet_; + } + + void SetLocalAet(const std::string& aet); + + const RemoteModalityParameters& GetRemoteModality() const + { + return remote_; + } + + void SetRemoteModality(const RemoteModalityParameters& remote); + + bool HasMoveOriginator() const + { + return moveOriginatorId_ != 0; + } + + const std::string& GetMoveOriginatorAet() const; + + uint16_t GetMoveOriginatorId() const; + + void SetMoveOriginator(const std::string& aet, + int id); + + virtual void ReleaseResources(); + + virtual void GetJobType(std::string& target) + { + target = "DicomModalityStore"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& target); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/LuaJobManager.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,268 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "LuaJobManager.h" + +#include "../OrthancInitialization.h" +#include "../../Core/Logging.h" + +#include "../../Core/JobsEngine/Operations/LogJobOperation.h" +#include "Operations/DeleteResourceOperation.h" +#include "Operations/ModifyInstanceOperation.h" +#include "Operations/StorePeerOperation.h" +#include "Operations/StoreScuOperation.h" +#include "Operations/SystemCallOperation.h" + +#include "../../Core/JobsEngine/Operations/NullOperationValue.h" +#include "../../Core/JobsEngine/Operations/StringOperationValue.h" +#include "Operations/DicomInstanceOperationValue.h" + +namespace Orthanc +{ + void LuaJobManager::SignalDone(const SequenceOfOperationsJob& job) + { + boost::mutex::scoped_lock lock(mutex_); + + if (&job == currentJob_) + { + currentId_.clear(); + currentJob_ = NULL; + } + } + + + LuaJobManager::LuaJobManager() : + currentJob_(NULL), + maxOperations_(1000), + priority_(0), + trailingTimeout_(5000) + { + dicomTimeout_ = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); + LOG(INFO) << "Lua: DICOM associations will be closed after " + << dicomTimeout_ << " seconds of inactivity"; + } + + + void LuaJobManager::SetMaxOperationsPerJob(size_t count) + { + boost::mutex::scoped_lock lock(mutex_); + maxOperations_ = count; + } + + + void LuaJobManager::SetPriority(int priority) + { + boost::mutex::scoped_lock lock(mutex_); + priority_ = priority; + } + + + void LuaJobManager::SetTrailingOperationTimeout(unsigned int timeout) + { + boost::mutex::scoped_lock lock(mutex_); + trailingTimeout_ = timeout; + } + + + void LuaJobManager::AwakeTrailingSleep() + { + boost::mutex::scoped_lock lock(mutex_); + + LOG(INFO) << "Awaking trailing sleep"; + + if (currentJob_ != NULL) + { + currentJob_->AwakeTrailingSleep(); + } + } + + + LuaJobManager::Lock::Lock(LuaJobManager& that, + JobsEngine& engine) : + that_(that), + lock_(that.mutex_), + engine_(engine) + { + if (that_.currentJob_ == NULL) + { + isNewJob_ = true; + } + else + { + jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_)); + + if (jobLock_->IsDone() || + jobLock_->GetOperationsCount() >= that_.maxOperations_) + { + jobLock_.reset(NULL); + isNewJob_ = true; + } + else + { + isNewJob_ = false; + } + } + + if (isNewJob_) + { + // Need to create a new job, as the previous one is either + // finished, or is getting too long + that_.currentJob_ = new SequenceOfOperationsJob; + that_.currentJob_->SetDescription("Lua"); + + { + jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_)); + jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_); + jobLock_->SetDicomAssociationTimeout(that_.dicomTimeout_ * 1000); // Milliseconds expected + } + } + + assert(jobLock_.get() != NULL); + } + + + LuaJobManager::Lock::~Lock() + { + bool isEmpty; + + assert(jobLock_.get() != NULL); + isEmpty = (isNewJob_ && + jobLock_->GetOperationsCount() == 0); + + jobLock_.reset(NULL); + + if (isNewJob_) + { + if (isEmpty) + { + // No operation was added, discard the newly created job + isNewJob_ = false; + delete that_.currentJob_; + that_.currentJob_ = NULL; + } + else + { + engine_.GetRegistry().Submit(that_.currentId_, that_.currentJob_, that_.priority_); + } + } + } + + + size_t LuaJobManager::Lock::AddDeleteResourceOperation(ServerContext& context) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation(new DeleteResourceOperation(context)); + } + + + size_t LuaJobManager::Lock::AddLogOperation() + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation(new LogJobOperation); + } + + + size_t LuaJobManager::Lock::AddStoreScuOperation(const std::string& localAet, + const RemoteModalityParameters& modality) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation(new StoreScuOperation(localAet, modality)); + } + + + size_t LuaJobManager::Lock::AddStorePeerOperation(const WebServiceParameters& peer) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation(new StorePeerOperation(peer)); + } + + + size_t LuaJobManager::Lock::AddSystemCallOperation(const std::string& command) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation(new SystemCallOperation(command)); + } + + + size_t LuaJobManager::Lock::AddSystemCallOperation + (const std::string& command, + const std::vector<std::string>& preArguments, + const std::vector<std::string>& postArguments) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation + (new SystemCallOperation(command, preArguments, postArguments)); + } + + + size_t LuaJobManager::Lock::AddModifyInstanceOperation(ServerContext& context, + DicomModification* modification) + { + assert(jobLock_.get() != NULL); + return jobLock_->AddOperation + (new ModifyInstanceOperation(context, RequestOrigin_Lua, modification)); + } + + + void LuaJobManager::Lock::AddNullInput(size_t operation) + { + assert(jobLock_.get() != NULL); + jobLock_->AddInput(operation, NullOperationValue()); + } + + + void LuaJobManager::Lock::AddStringInput(size_t operation, + const std::string& content) + { + assert(jobLock_.get() != NULL); + jobLock_->AddInput(operation, StringOperationValue(content)); + } + + + void LuaJobManager::Lock::AddDicomInstanceInput(size_t operation, + ServerContext& context, + const std::string& instanceId) + { + assert(jobLock_.get() != NULL); + jobLock_->AddInput(operation, DicomInstanceOperationValue(context, instanceId)); + } + + + void LuaJobManager::Lock::Connect(size_t operation1, + size_t operation2) + { + assert(jobLock_.get() != NULL); + jobLock_->Connect(operation1, operation2); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/LuaJobManager.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,115 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/DicomParsing/DicomModification.h" +#include "../../Core/JobsEngine/JobsEngine.h" +#include "../../Core/JobsEngine/Operations/SequenceOfOperationsJob.h" +#include "../../Core/WebServiceParameters.h" + +namespace Orthanc +{ + class ServerContext; + + class LuaJobManager : private SequenceOfOperationsJob::IObserver + { + private: + boost::mutex mutex_; + std::string currentId_; + SequenceOfOperationsJob* currentJob_; + size_t maxOperations_; + int priority_; + unsigned int trailingTimeout_; + unsigned int dicomTimeout_; + + virtual void SignalDone(const SequenceOfOperationsJob& job); + + public: + LuaJobManager(); + + void SetMaxOperationsPerJob(size_t count); + + void SetPriority(int priority); + + void SetTrailingOperationTimeout(unsigned int timeout); + + void AwakeTrailingSleep(); + + class Lock : public boost::noncopyable + { + private: + LuaJobManager& that_; + boost::mutex::scoped_lock lock_; + JobsEngine& engine_; + std::auto_ptr<SequenceOfOperationsJob::Lock> jobLock_; + bool isNewJob_; + + public: + Lock(LuaJobManager& that, + JobsEngine& engine); + + ~Lock(); + + size_t AddLogOperation(); + + size_t AddDeleteResourceOperation(ServerContext& context); + + size_t AddStoreScuOperation(const std::string& localAet, + const RemoteModalityParameters& modality); + + size_t AddStorePeerOperation(const WebServiceParameters& peer); + + size_t AddSystemCallOperation(const std::string& command); + + size_t AddSystemCallOperation(const std::string& command, + const std::vector<std::string>& preArguments, + const std::vector<std::string>& postArguments); + + size_t AddModifyInstanceOperation(ServerContext& context, + DicomModification* modification); + + void AddNullInput(size_t operation); + + void AddStringInput(size_t operation, + const std::string& content); + + void AddDicomInstanceInput(size_t operation, + ServerContext& context, + const std::string& instanceId); + + void Connect(size_t operation1, + size_t operation2); + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,72 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "DeleteResourceOperation.h" + +#include "DicomInstanceOperationValue.h" + +#include "../../../Core/Logging.h" +#include "../../../Core/OrthancException.h" + +namespace Orthanc +{ + void DeleteResourceOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + switch (input.GetType()) + { + case JobOperationValue::Type_DicomInstance: + { + const DicomInstanceOperationValue& instance = dynamic_cast<const DicomInstanceOperationValue&>(input); + LOG(INFO) << "Lua: Deleting instance: " << instance.GetId(); + + try + { + Json::Value tmp; + context_.DeleteResource(tmp, instance.GetId(), ResourceType_Instance); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: Unable to delete instance " << instance.GetId() << ": " << e.What(); + } + + break; + } + + default: + break; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/IJobOperation.h" + +#include "../../ServerContext.h" + +namespace Orthanc +{ + class DeleteResourceOperation : public IJobOperation + { + private: + ServerContext& context_; + + public: + DeleteResourceOperation(ServerContext& context) : + context_(context) + { + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "DeleteResource"; + } + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,84 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/JobOperationValue.h" + +#include "../../ServerContext.h" + +namespace Orthanc +{ + class DicomInstanceOperationValue : public JobOperationValue + { + private: + ServerContext& context_; + std::string id_; + + public: + DicomInstanceOperationValue(ServerContext& context, + const std::string& id) : + JobOperationValue(Type_DicomInstance), + context_(context), + id_(id) + { + } + + ServerContext& GetServerContext() const + { + return context_; + } + + const std::string& GetId() const + { + return id_; + } + + void ReadDicom(std::string& dicom) const + { + context_.ReadDicom(dicom, id_); + } + + virtual JobOperationValue* Clone() const + { + return new DicomInstanceOperationValue(context_, id_); + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "DicomInstance"; + target["ID"] = id_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,153 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ModifyInstanceOperation.h" + +#include "DicomInstanceOperationValue.h" + +#include "../../../Core/Logging.h" +#include "../../../Core/SerializationToolbox.h" + +namespace Orthanc +{ + ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context, + RequestOrigin origin, + DicomModification* modification) : + context_(context), + origin_(origin), + modification_(modification) + { + if (modification == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + modification_->SetAllowManualIdentifiers(true); + + if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) + { + modification_->SetLevel(ResourceType_Patient); + } + else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + modification_->SetLevel(ResourceType_Study); + } + else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + modification_->SetLevel(ResourceType_Series); + } + else + { + modification_->SetLevel(ResourceType_Instance); + } + + if (origin_ != RequestOrigin_Lua) + { + // TODO If issued from HTTP, "remoteIp" and "username" must be provided + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ModifyInstanceOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + if (input.GetType() != JobOperationValue::Type_DicomInstance) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + const DicomInstanceOperationValue& instance = + dynamic_cast<const DicomInstanceOperationValue&>(input); + + LOG(INFO) << "Lua: Modifying instance " << instance.GetId(); + + std::auto_ptr<ParsedDicomFile> modified; + + { + ServerContext::DicomCacheLocker lock(context_, instance.GetId()); + modified.reset(lock.GetDicom().Clone(true)); + } + + try + { + modification_->Apply(*modified); + + DicomInstanceToStore toStore; + assert(origin_ == RequestOrigin_Lua); + toStore.SetOrigin(DicomInstanceOrigin::FromLua()); + toStore.SetParsedDicomFile(*modified); + + // TODO other metadata + toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, instance.GetId()); + + std::string modifiedId; + context_.Store(modifiedId, toStore); + + // Only chain with other commands if this command succeeds + outputs.Append(new DicomInstanceOperationValue(instance.GetServerContext(), modifiedId)); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: Unable to modify instance " << instance.GetId() + << ": " << e.What(); + } + } + + + void ModifyInstanceOperation::Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "ModifyInstance"; + target["Origin"] = EnumerationToString(origin_); + modification_->Serialize(target["Modification"]); + } + + + ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context, + const Json::Value& serialized) : + context_(context) + { + if (SerializationToolbox::ReadString(serialized, "Type") != "ModifyInstance" || + !serialized.isMember("Modification")) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, "Origin")); + + modification_.reset(new DicomModification(serialized["Modification"])); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,75 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/IJobOperation.h" +#include "../../../Core/DicomParsing/DicomModification.h" + +#include "../../ServerContext.h" + +namespace Orthanc +{ + class ModifyInstanceOperation : public IJobOperation + { + private: + ServerContext& context_; + RequestOrigin origin_; + std::auto_ptr<DicomModification> modification_; + + public: + ModifyInstanceOperation(ServerContext& context, + RequestOrigin origin, + DicomModification* modification); // Takes ownership + + ModifyInstanceOperation(ServerContext& context, + const Json::Value& serialized); + + const RequestOrigin& GetRequestOrigin() const + { + return origin_; + } + + const DicomModification& GetModification() const + { + return *modification_; + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& target) const; + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "StorePeerOperation.h" + +#include "DicomInstanceOperationValue.h" + +#include "../../../Core/Logging.h" +#include "../../../Core/OrthancException.h" +#include "../../../Core/HttpClient.h" +#include "../../../Core/SerializationToolbox.h" + +namespace Orthanc +{ + void StorePeerOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + // Configure the HTTP client + HttpClient client(peer_, "instances"); + client.SetMethod(HttpMethod_Post); + + if (input.GetType() != JobOperationValue::Type_DicomInstance) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + const DicomInstanceOperationValue& instance = + dynamic_cast<const DicomInstanceOperationValue&>(input); + + LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to Orthanc peer \"" + << peer_.GetUrl() << "\""; + + try + { + instance.ReadDicom(client.GetBody()); + + std::string answer; + if (!client.Apply(answer)) + { + LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId() + << " to Orthanc peer \"" << peer_.GetUrl(); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId() + << " to Orthanc peer \"" << peer_.GetUrl() << "\": " << e.What(); + } + + outputs.Append(input.Clone()); + } + + + void StorePeerOperation::Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "StorePeer"; + peer_.Serialize(result["Peer"]); + } + + + StorePeerOperation::StorePeerOperation(const Json::Value& serialized) + { + if (SerializationToolbox::ReadString(serialized, "Type") != "StorePeer" || + !serialized.isMember("Peer")) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + peer_ = WebServiceParameters(serialized["Peer"]); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/IJobOperation.h" +#include "../../../Core/WebServiceParameters.h" + +namespace Orthanc +{ + class StorePeerOperation : public IJobOperation + { + private: + WebServiceParameters peer_; + + public: + StorePeerOperation(const WebServiceParameters& peer) : + peer_(peer) + { + } + + StorePeerOperation(const Json::Value& serialized); + + const WebServiceParameters& GetPeer() const + { + return peer_; + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& result) const; + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,105 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "StoreScuOperation.h" + +#include "DicomInstanceOperationValue.h" + +#include "../../../Core/Logging.h" +#include "../../../Core/OrthancException.h" +#include "../../../Core/SerializationToolbox.h" + +namespace Orthanc +{ + void StoreScuOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + std::auto_ptr<IDicomConnectionManager::IResource> resource + (connectionManager.AcquireConnection(localAet_, modality_)); + + if (resource.get() == NULL) + { + LOG(ERROR) << "Lua: Cannot connect to modality: " << modality_.GetApplicationEntityTitle(); + return; + } + + if (input.GetType() != JobOperationValue::Type_DicomInstance) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + const DicomInstanceOperationValue& instance = + dynamic_cast<const DicomInstanceOperationValue&>(input); + + LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to modality \"" + << modality_.GetApplicationEntityTitle() << "\""; + + try + { + std::string dicom; + instance.ReadDicom(dicom); + resource->GetConnection().Store(dicom); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId() << " to modality \"" + << modality_.GetApplicationEntityTitle() << "\": " << e.What(); + } + + outputs.Append(input.Clone()); + } + + + void StoreScuOperation::Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "StoreScu"; + result["LocalAET"] = localAet_; + modality_.Serialize(result["Modality"]); + } + + + StoreScuOperation::StoreScuOperation(const Json::Value& serialized) + { + if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" || + !serialized.isMember("LocalAET")) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + localAet_ = SerializationToolbox::ReadString(serialized, "LocalAET"); + modality_ = RemoteModalityParameters(serialized["Modality"]); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,74 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/IJobOperation.h" +#include "../../../Core/DicomNetworking/RemoteModalityParameters.h" + +namespace Orthanc +{ + class StoreScuOperation : public IJobOperation + { + private: + std::string localAet_; + RemoteModalityParameters modality_; + + public: + StoreScuOperation(const std::string& localAet, + const RemoteModalityParameters& modality) : + localAet_(localAet), + modality_(modality) + { + } + + StoreScuOperation(const Json::Value& serialized); + + const std::string& GetLocalAet() const + { + return localAet_; + } + + const RemoteModalityParameters& GetRemoteModality() const + { + return modality_; + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& manager); + + virtual void Serialize(Json::Value& result) const; + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,165 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "SystemCallOperation.h" + +#include "DicomInstanceOperationValue.h" + +#include "../../../Core/JobsEngine/Operations/StringOperationValue.h" +#include "../../../Core/Logging.h" +#include "../../../Core/OrthancException.h" +#include "../../../Core/SerializationToolbox.h" +#include "../../../Core/TemporaryFile.h" +#include "../../../Core/Toolbox.h" + +namespace Orthanc +{ + const std::string& SystemCallOperation::GetPreArgument(size_t i) const + { + if (i >= preArguments_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return preArguments_[i]; + } + } + + + const std::string& SystemCallOperation::GetPostArgument(size_t i) const + { + if (i >= postArguments_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return postArguments_[i]; + } + } + + + void SystemCallOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + std::vector<std::string> arguments = preArguments_; + + arguments.reserve(arguments.size() + postArguments_.size() + 1); + + std::auto_ptr<TemporaryFile> tmp; + + switch (input.GetType()) + { + case JobOperationValue::Type_DicomInstance: + { + const DicomInstanceOperationValue& instance = + dynamic_cast<const DicomInstanceOperationValue&>(input); + + std::string dicom; + instance.ReadDicom(dicom); + + tmp.reset(new TemporaryFile); + tmp->Write(dicom); + + arguments.push_back(tmp->GetPath()); + break; + } + + case JobOperationValue::Type_String: + { + const StringOperationValue& value = + dynamic_cast<const StringOperationValue&>(input); + + arguments.push_back(value.GetContent()); + break; + } + + case JobOperationValue::Type_Null: + break; + + default: + throw OrthancException(ErrorCode_BadParameterType); + } + + for (size_t i = 0; i < postArguments_.size(); i++) + { + arguments.push_back(postArguments_[i]); + } + + std::string info = command_; + for (size_t i = 0; i < arguments.size(); i++) + { + info += " " + arguments[i]; + } + + LOG(INFO) << "Lua: System call: \"" << info << "\""; + + try + { + SystemToolbox::ExecuteSystemCommand(command_, arguments); + + // Only chain with other commands if this operation succeeds + outputs.Append(input.Clone()); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: Failed system call - \"" << info << "\": " << e.What(); + } + } + + + void SystemCallOperation::Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "SystemCall"; + result["Command"] = command_; + SerializationToolbox::WriteArrayOfStrings(result, preArguments_, "PreArguments"); + SerializationToolbox::WriteArrayOfStrings(result, postArguments_, "PostArguments"); + } + + + SystemCallOperation::SystemCallOperation(const Json::Value& serialized) + { + if (SerializationToolbox::ReadString(serialized, "Type") != "SystemCall") + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + command_ = SerializationToolbox::ReadString(serialized, "Command"); + SerializationToolbox::ReadArrayOfStrings(preArguments_, serialized, "PreArguments"); + SerializationToolbox::ReadArrayOfStrings(postArguments_, serialized, "PostArguments"); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../../Core/JobsEngine/Operations/IJobOperation.h" + +#include <string> + +namespace Orthanc +{ + class SystemCallOperation : public IJobOperation + { + private: + std::string command_; + std::vector<std::string> preArguments_; + std::vector<std::string> postArguments_; + + public: + SystemCallOperation(const std::string& command) : + command_(command) + { + } + + SystemCallOperation(const Json::Value& serialized); + + SystemCallOperation(const std::string& command, + const std::vector<std::string>& preArguments, + const std::vector<std::string>& postArguments) : + command_(command), + preArguments_(preArguments), + postArguments_(postArguments) + { + } + + void AddPreArgument(const std::string& argument) + { + preArguments_.push_back(argument); + } + + void AddPostArgument(const std::string& argument) + { + postArguments_.push_back(argument); + } + + const std::string& GetCommand() const + { + return command_; + } + + size_t GetPreArgumentsCount() const + { + return preArguments_.size(); + } + + size_t GetPostArgumentsCount() const + { + return postArguments_.size(); + } + + const std::string& GetPreArgument(size_t i) const; + + const std::string& GetPostArgument(size_t i) const; + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& result) const; + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,121 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "OrthancJobUnserializer.h" + +#include "../../Core/Logging.h" +#include "../../Core/OrthancException.h" +#include "../../Core/SerializationToolbox.h" + +#include "Operations/DeleteResourceOperation.h" +#include "Operations/DicomInstanceOperationValue.h" +#include "Operations/ModifyInstanceOperation.h" +#include "Operations/StorePeerOperation.h" +#include "Operations/StoreScuOperation.h" +#include "Operations/SystemCallOperation.h" + +#include "DicomModalityStoreJob.h" +#include "OrthancPeerStoreJob.h" +#include "ResourceModificationJob.h" + +namespace Orthanc +{ + IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "DicomModalityStore") + { + return new DicomModalityStoreJob(context_, source); + } + else if (type == "OrthancPeerStore") + { + return new OrthancPeerStoreJob(context_, source); + } + else if (type == "ResourceModification") + { + return new ResourceModificationJob(context_, source); + } + else + { + return GenericJobUnserializer::UnserializeJob(source); + } + } + + + IJobOperation* OrthancJobUnserializer::UnserializeOperation(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "DeleteResource") + { + return new DeleteResourceOperation(context_); + } + else if (type == "ModifyInstance") + { + return new ModifyInstanceOperation(context_, source); + } + else if (type == "StorePeer") + { + return new StorePeerOperation(source); + } + else if (type == "StoreScu") + { + return new StoreScuOperation(source); + } + else if (type == "SystemCall") + { + return new SystemCallOperation(source); + } + else + { + return GenericJobUnserializer::UnserializeOperation(source); + } + } + + + JobOperationValue* OrthancJobUnserializer::UnserializeValue(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "DicomInstance") + { + return new DicomInstanceOperationValue(context_, SerializationToolbox::ReadString(source, "ID")); + } + else + { + return GenericJobUnserializer::UnserializeValue(source); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,58 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../ServerContext.h" +#include "../../Core/JobsEngine/GenericJobUnserializer.h" + +namespace Orthanc +{ + class OrthancJobUnserializer : public GenericJobUnserializer + { + private: + ServerContext& context_; + + public: + OrthancJobUnserializer(ServerContext& context) : + context_(context) + { + } + + virtual IJob* UnserializeJob(const Json::Value& value); + + virtual IJobOperation* UnserializeOperation(const Json::Value& value); + + virtual JobOperationValue* UnserializeValue(const Json::Value& value); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,129 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "OrthancPeerStoreJob.h" + +#include "../../Core/Logging.h" + + +namespace Orthanc +{ + bool OrthancPeerStoreJob::HandleInstance(const std::string& instance) + { + //boost::this_thread::sleep(boost::posix_time::milliseconds(500)); + + if (client_.get() == NULL) + { + client_.reset(new HttpClient(peer_, "instances")); + client_->SetMethod(HttpMethod_Post); + } + + LOG(INFO) << "Sending instance " << instance << " to peer \"" + << peer_.GetUrl() << "\""; + + try + { + context_.ReadDicom(client_->GetBody(), instance); + } + catch (OrthancException& e) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instance; + return false; + } + + std::string answer; + if (client_->Apply(answer)) + { + return true; + } + else + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + + void OrthancPeerStoreJob::SetPeer(const WebServiceParameters& peer) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + peer_ = peer; + } + } + + + void OrthancPeerStoreJob::ReleaseResources() // For pausing jobs + { + client_.reset(NULL); + } + + + void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) + { + SetOfInstancesJob::GetPublicContent(value); + + Json::Value v; + peer_.ToJson(v, false /* don't include passwords */); + value["Peer"] = v; + } + + + static const char* PEER = "Peer"; + + OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), + context_(context) + { + peer_ = WebServiceParameters(serialized[PEER]); + } + + + bool OrthancPeerStoreJob::Serialize(Json::Value& target) + { + if (!SetOfInstancesJob::Serialize(target)) + { + return false; + } + else + { + peer_.Serialize(target[PEER]); + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" +#include "../../Core/HttpClient.h" + +#include "../ServerContext.h" + + +namespace Orthanc +{ + class OrthancPeerStoreJob : public SetOfInstancesJob + { + private: + ServerContext& context_; + WebServiceParameters peer_; + std::auto_ptr<HttpClient> client_; + + protected: + virtual bool HandleInstance(const std::string& instance); + + public: + OrthancPeerStoreJob(ServerContext& context) : + context_(context) + { + } + + OrthancPeerStoreJob(ServerContext& context, + const Json::Value& serialize); + + void SetPeer(const WebServiceParameters& peer); + + const WebServiceParameters& GetPeer() const + { + return peer_; + } + + virtual void ReleaseResources(); // For pausing jobs + + virtual void GetJobType(std::string& target) + { + target = "OrthancPeerStore"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& target); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,321 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ResourceModificationJob.h" + +#include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" + +namespace Orthanc +{ + ResourceModificationJob::Output::Output(ResourceType level) : + level_(level), + isFirst_(true) + { + if (level_ != ResourceType_Patient && + level_ != ResourceType_Study && + level_ != ResourceType_Series) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void ResourceModificationJob::Output::Update(DicomInstanceHasher& hasher) + { + boost::mutex::scoped_lock lock(mutex_); + + if (isFirst_) + { + switch (level_) + { + case ResourceType_Series: + id_ = hasher.HashSeries(); + break; + + case ResourceType_Study: + id_ = hasher.HashStudy(); + break; + + case ResourceType_Patient: + id_ = hasher.HashPatient(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + patientId_ = hasher.HashPatient(); + isFirst_ = false; + } + } + + + bool ResourceModificationJob::Output::Format(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + + if (isFirst_) + { + return false; + } + else + { + target = Json::objectValue; + target["Type"] = EnumerationToString(level_); + target["ID"] = id_; + target["Path"] = GetBasePath(level_, id_); + target["PatientID"] = patientId_; + return true; + } + } + + + bool ResourceModificationJob::Output::GetIdentifier(std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + + if (isFirst_) + { + return false; + } + else + { + id = id_; + return true; + } + } + + + bool ResourceModificationJob::HandleInstance(const std::string& instance) + { + if (modification_.get() == NULL) + { + LOG(ERROR) << "No modification was provided for this job"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + + LOG(INFO) << "Modifying instance in a job: " << instance; + + std::auto_ptr<ServerContext::DicomCacheLocker> locker; + + try + { + locker.reset(new ServerContext::DicomCacheLocker(context_, instance)); + } + catch (OrthancException&) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instance; + return false; + } + + + ParsedDicomFile& original = locker->GetDicom(); + DicomInstanceHasher originalHasher = original.GetHasher(); + + + /** + * Compute the resulting DICOM instance. + **/ + + std::auto_ptr<ParsedDicomFile> modified(original.Clone(true)); + modification_->Apply(*modified); + + DicomInstanceToStore toStore; + toStore.SetOrigin(origin_); + toStore.SetParsedDicomFile(*modified); + + + /** + * Prepare the metadata information to associate with the + * resulting DICOM instance (AnonymizedFrom/ModifiedFrom). + **/ + + DicomInstanceHasher modifiedHasher = modified->GetHasher(); + + MetadataType metadataType = (isAnonymization_ ? + MetadataType_AnonymizedFrom : + MetadataType_ModifiedFrom); + + if (originalHasher.HashSeries() != modifiedHasher.HashSeries()) + { + toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries()); + } + + if (originalHasher.HashStudy() != modifiedHasher.HashStudy()) + { + toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy()); + } + + if (originalHasher.HashPatient() != modifiedHasher.HashPatient()) + { + toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient()); + } + + assert(instance == originalHasher.HashInstance()); + toStore.AddMetadata(ResourceType_Instance, metadataType, instance); + + + /** + * Store the resulting DICOM instance into the Orthanc store. + **/ + + std::string modifiedInstance; + if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << instance; + throw OrthancException(ErrorCode_CannotStoreInstance); + } + + // Sanity checks in debug mode + assert(modifiedInstance == modifiedHasher.HashInstance()); + + + if (output_.get() != NULL) + { + output_->Update(modifiedHasher); + } + + return true; + } + + + + void ResourceModificationJob::SetModification(DicomModification* modification, + bool isAnonymization) + { + if (modification == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + modification_.reset(modification); + isAnonymization_ = isAnonymization; + } + } + + + void ResourceModificationJob::SetOutput(boost::shared_ptr<Output>& output) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + output_ = output; + } + } + + + void ResourceModificationJob::SetOrigin(const DicomInstanceOrigin& origin) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + origin_ = origin; + } + } + + + void ResourceModificationJob::SetOrigin(const RestApiCall& call) + { + SetOrigin(DicomInstanceOrigin::FromRest(call)); + } + + + const DicomModification& ResourceModificationJob::GetModification() const + { + if (modification_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *modification_; + } + } + + + void ResourceModificationJob::GetPublicContent(Json::Value& value) + { + SetOfInstancesJob::GetPublicContent(value); + + value["IsAnonymization"] = isAnonymization_; + } + + + static const char* MODIFICATION = "Modification"; + static const char* ORIGIN = "Origin"; + static const char* IS_ANONYMIZATION = "IsAnonymization"; + + + ResourceModificationJob::ResourceModificationJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), + context_(context) + { + isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION); + origin_ = DicomInstanceOrigin(serialized[ORIGIN]); + modification_.reset(new DicomModification(serialized[MODIFICATION])); + } + + bool ResourceModificationJob::Serialize(Json::Value& value) + { + if (!SetOfInstancesJob::Serialize(value)) + { + return false; + } + else + { + value[IS_ANONYMIZATION] = isAnonymization_; + origin_.Serialize(value[ORIGIN]); + + Json::Value tmp; + modification_->Serialize(tmp); + value[MODIFICATION] = tmp; + + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,122 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class ResourceModificationJob : public SetOfInstancesJob + { + public: + class Output : public boost::noncopyable + { + private: + boost::mutex mutex_; + ResourceType level_; + bool isFirst_; + std::string id_; + std::string patientId_; + + public: + Output(ResourceType level); + + ResourceType GetLevel() const + { + return level_; + } + + void Update(DicomInstanceHasher& hasher); + + bool Format(Json::Value& target); + + bool GetIdentifier(std::string& id); + }; + + private: + ServerContext& context_; + std::auto_ptr<DicomModification> modification_; + boost::shared_ptr<Output> output_; + bool isAnonymization_; + DicomInstanceOrigin origin_; + + protected: + virtual bool HandleInstance(const std::string& instance); + + public: + ResourceModificationJob(ServerContext& context) : + context_(context), + isAnonymization_(false) + { + } + + ResourceModificationJob(ServerContext& context, + const Json::Value& serialized); + + void SetModification(DicomModification* modification, // Takes ownership + bool isAnonymization); + + void SetOutput(boost::shared_ptr<Output>& output); + + void SetOrigin(const DicomInstanceOrigin& origin); + + void SetOrigin(const RestApiCall& call); + + const DicomModification& GetModification() const; + + bool IsAnonymization() const + { + return isAnonymization_; + } + + const DicomInstanceOrigin& GetOrigin() const + { + return origin_; + } + + virtual void ReleaseResources() + { + } + + virtual void GetJobType(std::string& target) + { + target = "ResourceModification"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& value); + }; +}
--- a/OrthancServer/main.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/OrthancServer/main.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -42,7 +42,6 @@ #include "../Core/Lua/LuaFunctionCall.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/DicomNetworking/DicomServer.h" -#include "../Core/DicomNetworking/ReusableDicomUserConnection.h" #include "OrthancInitialization.h" #include "ServerContext.h" #include "OrthancFindRequestHandler.h" @@ -76,7 +75,8 @@ if (dicomFile.size() > 0) { DicomInstanceToStore toStore; - toStore.SetDicomProtocolOrigin(remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()); + toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol + (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str())); toStore.SetBuffer(dicomFile); toStore.SetSummary(dicomSummary); toStore.SetJson(dicomJson); @@ -168,9 +168,9 @@ class OrthancApplicationEntityFilter : public IApplicationEntityFilter { private: - ServerContext& context_; - bool alwaysAllowEcho_; - bool alwaysAllowStore_; + ServerContext& context_; + bool alwaysAllowEcho_; + bool alwaysAllowStore_; public: OrthancApplicationEntityFilter(ServerContext& context) : @@ -264,13 +264,13 @@ } { - std::string lua = "Is" + configuration; + std::string name = "Is" + configuration; - LuaScripting::Locker locker(context_.GetLua()); + LuaScripting::Lock lock(context_.GetLuaScripting()); - if (locker.GetLua().IsExistingFunction(lua.c_str())) + if (lock.GetLua().IsExistingFunction(name.c_str())) { - LuaFunctionCall call(locker.GetLua(), lua.c_str()); + LuaFunctionCall call(lock.GetLua(), name.c_str()); call.PushString(remoteAet); call.PushString(remoteIp); call.PushString(calledAet); @@ -291,11 +291,11 @@ { std::string lua = "Is" + std::string(configuration); - LuaScripting::Locker locker(context_.GetLua()); + LuaScripting::Lock lock(context_.GetLuaScripting()); - if (locker.GetLua().IsExistingFunction(lua.c_str())) + if (lock.GetLua().IsExistingFunction(lua.c_str())) { - LuaFunctionCall call(locker.GetLua(), lua.c_str()); + LuaFunctionCall call(lock.GetLua(), lua.c_str()); call.PushString(remoteAet); call.PushString(remoteIp); call.PushString(calledAet); @@ -311,7 +311,7 @@ class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter { private: - ServerContext& context_; + ServerContext& context_; OrthancPlugins* plugins_; public: @@ -327,7 +327,7 @@ const char* ip, const char* username, const IHttpHandler::Arguments& httpHeaders, - const IHttpHandler::GetArguments& getArguments) const + const IHttpHandler::GetArguments& getArguments) { if (plugins_ != NULL && !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments)) @@ -337,12 +337,12 @@ static const char* HTTP_FILTER = "IncomingHttpRequestFilter"; - LuaScripting::Locker locker(context_.GetLua()); + LuaScripting::Lock lock(context_.GetLuaScripting()); // Test if the instance must be filtered out - if (locker.GetLua().IsExistingFunction(HTTP_FILTER)) + if (lock.GetLua().IsExistingFunction(HTTP_FILTER)) { - LuaFunctionCall call(locker.GetLua(), HTTP_FILTER); + LuaFunctionCall call(lock.GetLua(), HTTP_FILTER); switch (method) { @@ -488,6 +488,8 @@ << " --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 << " --version\t\toutput version information and exit" << std::endl << std::endl << "Exit status:" << std::endl @@ -574,6 +576,7 @@ PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header"); PrintErrorCode(ErrorCode_NullPointer, "Cannot handle a NULL pointer"); PrintErrorCode(ErrorCode_DatabaseUnavailable, "The database is currently not available (probably a transient situation)"); + PrintErrorCode(ErrorCode_CanceledJob, "This job was canceled"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); @@ -640,25 +643,6 @@ -static void LoadLuaScripts(ServerContext& context) -{ - std::list<std::string> luaScripts; - Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts"); - for (std::list<std::string>::const_iterator - it = luaScripts.begin(); it != luaScripts.end(); ++it) - { - std::string path = Configuration::InterpretStringParameterAsPath(*it); - LOG(WARNING) << "Installing the Lua scripts from: " << path; - std::string script; - SystemToolbox::ReadFile(script, path); - - LuaScripting::Locker locker(context.GetLua()); - locker.GetLua().Execute(script); - } -} - - - #if ORTHANC_ENABLE_PLUGINS == 1 static void LoadPlugins(OrthancPlugins& plugins) { @@ -689,7 +673,8 @@ } #endif - context.GetLua().Execute("Initialize"); + context.GetLuaScripting().Start(); + context.GetLuaScripting().Execute("Initialize"); bool restart; @@ -723,7 +708,8 @@ } } - context.GetLua().Execute("Finalize"); + context.GetLuaScripting().Execute("Finalize"); + context.GetLuaScripting().Stop(); #if ORTHANC_ENABLE_PLUGINS == 1 if (context.HasPlugins()) @@ -972,7 +958,8 @@ static bool ConfigureServerContext(IDatabaseWrapper& database, IStorageArea& storageArea, - OrthancPlugins *plugins) + OrthancPlugins *plugins, + bool loadJobsFromDatabase) { // These configuration options must be set before creating the // ServerContext, otherwise the possible Lua scripts will not be @@ -985,7 +972,7 @@ DicomUserConnection::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScuTimeout", 10)); - ServerContext context(database, storageArea); + ServerContext context(database, storageArea, false /* not running unit tests */, loadJobsFromDatabase); context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false)); context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true)); @@ -1008,7 +995,11 @@ context.GetIndex().SetMaximumStorageSize(0); } - LoadLuaScripts(context); + LOG(INFO) << "Initializing Lua for the event handler"; + context.GetLuaScripting().LoadGlobalConfiguration(); + + context.GetJobsEngine().GetRegistry().SetMaxCompletedJobs + (Configuration::GetGlobalUnsignedIntegerParameter("JobsHistorySize", 10)); #if ORTHANC_ENABLE_PLUGINS == 1 if (plugins) @@ -1052,7 +1043,8 @@ static bool ConfigureDatabase(IDatabaseWrapper& database, IStorageArea& storageArea, OrthancPlugins *plugins, - bool upgradeDatabase) + bool upgradeDatabase, + bool loadJobsFromDatabase) { database.Open(); @@ -1071,7 +1063,8 @@ throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } - bool success = ConfigureServerContext(database, storageArea, plugins); + bool success = ConfigureServerContext + (database, storageArea, plugins, loadJobsFromDatabase); database.Close(); @@ -1081,7 +1074,8 @@ static bool ConfigurePlugins(int argc, char* argv[], - bool upgradeDatabase) + bool upgradeDatabase, + bool loadJobsFromDatabase) { std::auto_ptr<IDatabaseWrapper> databasePtr; std::auto_ptr<IStorageArea> storage; @@ -1116,14 +1110,16 @@ assert(database != NULL); assert(storage.get() != NULL); - return ConfigureDatabase(*database, *storage, &plugins, upgradeDatabase); + return ConfigureDatabase(*database, *storage, &plugins, + upgradeDatabase, loadJobsFromDatabase); #elif ORTHANC_ENABLE_PLUGINS == 0 // The plugins are disabled databasePtr.reset(Configuration::CreateDatabaseWrapper()); storage.reset(Configuration::CreateStorageArea()); - return ConfigureDatabase(*databasePtr, *storage, NULL, upgradeDatabase); + return ConfigureDatabase(*databasePtr, *storage, NULL, + upgradeDatabase, loadJobsFromDatabase); #else # error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1 @@ -1133,9 +1129,10 @@ static bool StartOrthanc(int argc, char* argv[], - bool upgradeDatabase) + bool upgradeDatabase, + bool loadJobsFromDatabase) { - return ConfigurePlugins(argc, argv, upgradeDatabase); + return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase); } @@ -1152,6 +1149,7 @@ Logging::Initialize(); bool upgradeDatabase = false; + bool loadJobsFromDatabase = true; const char* configurationFile = NULL; @@ -1242,6 +1240,10 @@ { upgradeDatabase = true; } + else if (argument == "--no-jobs") + { + loadJobsFromDatabase = false; + } else if (boost::starts_with(argument, "--config=")) { // TODO WHAT IS THE ENCODING? @@ -1305,7 +1307,7 @@ { OrthancInitialize(configurationFile); - bool restart = StartOrthanc(argc, argv, upgradeDatabase); + bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase); if (restart) { OrthancFinalize();
--- a/Plugins/Engine/OrthancPlugins.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -1544,7 +1544,7 @@ switch (service) { case _OrthancPluginService_GetInstanceRemoteAet: - *p.resultString = instance.GetRemoteAet(); + *p.resultString = instance.GetOrigin().GetRemoteAetC(); return; case _OrthancPluginService_GetInstanceSize: @@ -1585,7 +1585,7 @@ } case _OrthancPluginService_GetInstanceOrigin: // New in Orthanc 0.9.5 - *p.resultOrigin = Plugins::Convert(instance.GetRequestOrigin()); + *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin()); return; default: @@ -3115,7 +3115,7 @@ const char* ip, const char* username, const IHttpHandler::Arguments& httpHeaders, - const IHttpHandler::GetArguments& getArguments) const + const IHttpHandler::GetArguments& getArguments) { OrthancPluginHttpMethod cMethod = Plugins::Convert(method);
--- a/Plugins/Engine/OrthancPlugins.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Mon Jun 11 16:30:13 2018 +0200 @@ -284,7 +284,7 @@ const char* ip, const char* username, const IHttpHandler::Arguments& httpHeaders, - const IHttpHandler::GetArguments& getArguments) const; + const IHttpHandler::GetArguments& getArguments); bool HasFindHandler();
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Thu Jun 07 17:15:55 2018 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Mon Jun 11 16:30:13 2018 +0200 @@ -235,6 +235,7 @@ OrthancPluginErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, OrthancPluginErrorCode_NullPointer = 35 /*!< Cannot handle a NULL pointer */, OrthancPluginErrorCode_DatabaseUnavailable = 36 /*!< The database is currently not available (probably a transient situation) */, + OrthancPluginErrorCode_CanceledJob = 37 /*!< This job was canceled */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */,
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Thu Jun 07 17:15:55 2018 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Mon Jun 11 16:30:13 2018 +0200 @@ -130,6 +130,7 @@ ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp ${ORTHANC_ROOT}/Core/Enumerations.cpp + ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp ${ORTHANC_ROOT}/Core/Images/Font.cpp ${ORTHANC_ROOT}/Core/Images/FontRegistry.cpp ${ORTHANC_ROOT}/Core/Images/IImageWriter.cpp @@ -137,7 +138,16 @@ ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/GenericJobUnserializer.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/JobInfo.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/JobStatus.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/JobStepResult.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/Operations/JobOperationValues.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/Operations/LogJobOperation.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/SetOfInstancesJob.cpp ${ORTHANC_ROOT}/Core/Logging.cpp + ${ORTHANC_ROOT}/Core/SerializationToolbox.cpp ${ORTHANC_ROOT}/Core/Toolbox.cpp ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp ) @@ -423,13 +433,12 @@ ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/ReusableDicomUserConnection.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/StoreScp.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp ) else() add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=0) @@ -498,11 +507,9 @@ list(APPEND ORTHANC_CORE_SOURCES_INTERNAL ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp - ${ORTHANC_ROOT}/Core/MultiThreading/BagOfTasksProcessor.cpp - ${ORTHANC_ROOT}/Core/MultiThreading/Mutex.cpp - ${ORTHANC_ROOT}/Core/MultiThreading/ReaderWriterLock.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/JobsEngine.cpp + ${ORTHANC_ROOT}/Core/JobsEngine/JobsRegistry.cpp ${ORTHANC_ROOT}/Core/MultiThreading/RunnableWorkersPool.cpp - ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp ${ORTHANC_ROOT}/Core/SharedLibrary.cpp ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
--- a/Resources/Configuration.json Thu Jun 07 17:15:55 2018 +0200 +++ b/Resources/Configuration.json Mon Jun 11 16:30:13 2018 +0200 @@ -43,6 +43,11 @@ "Plugins" : [ ], + // Maximum number of processing jobs that are simultanously running + // at any given time. A value of "0" indicates to use all the + // available CPU logical cores. To emulate Orthanc <= 1.3.2, set + // this value to "1". + "ConcurrentJobs" : 2, /** @@ -310,10 +315,11 @@ // this option might prevent the upgrade to newer versions of Orthanc. "StoreDicom" : true, - // DICOM associations are kept open as long as new DICOM commands - // are issued. This option sets the number of seconds of inactivity - // to wait before automatically closing a DICOM association. If set - // to 0, the connection is closed immediately. + // DICOM associations initiated by Lua scripts are kept open as long + // as new DICOM commands are issued. This option sets the number of + // seconds of inactivity to wait before automatically closing a + // DICOM association used by Lua. If set to 0, the connection is + // closed immediately. "DicomAssociationCloseDelay" : 5, // Maximum number of query/retrieve DICOM requests that are @@ -371,5 +377,16 @@ // "00e1,10c2" : [ "UI", "PET-CT Multi Modality Name", 1, 1, "ELSCINT1" ] // "7053,1003" : [ "ST", "Original Image Filename", 1, 1, "Philips PET Private Group" ] // "2001,5f" : [ "SQ", "StackSequence", 1, 1, "Philips Imaging DD 001" ] - } + }, + + // Whether to run DICOM C-Move operations synchronously. If set to + // "false" (the default), each incoming C-Move request results in + // creating a new background job. Until Orthanc 1.3.2, the default + // behavior was to use synchronous C-Move. + "SynchronousCMove" : false, + + // Maximum number of completed jobs that are kept in memory. A + // processing job is considered as complete once it is tagged as + // "Success" or "Failure". + "JobsHistorySize" : 10 }
--- a/Resources/ErrorCodes.json Thu Jun 07 17:15:55 2018 +0200 +++ b/Resources/ErrorCodes.json Mon Jun 11 16:30:13 2018 +0200 @@ -207,6 +207,11 @@ "Name": "DatabaseUnavailable", "Description": "The database is currently not available (probably a transient situation)" }, + { + "Code": 37, + "Name": "CanceledJob", + "Description": "This job was canceled" + },
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/BagOfTasks.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,84 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../ICommand.h" + +#include <list> +#include <cstddef> + +namespace Orthanc +{ + class BagOfTasks : public boost::noncopyable + { + private: + typedef std::list<ICommand*> Tasks; + + Tasks tasks_; + + public: + ~BagOfTasks() + { + for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it) + { + delete *it; + } + } + + ICommand* Pop() + { + ICommand* task = tasks_.front(); + tasks_.pop_front(); + return task; + } + + void Push(ICommand* task) // Takes ownership + { + if (task != NULL) + { + tasks_.push_back(task); + } + } + + size_t GetSize() const + { + return tasks_.size(); + } + + bool IsEmpty() const + { + return tasks_.empty(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,277 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "BagOfTasksProcessor.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +#include <stdio.h> + +namespace Orthanc +{ + class BagOfTasksProcessor::Task : public IDynamicObject + { + private: + uint64_t bag_; + std::auto_ptr<ICommand> command_; + + public: + Task(uint64_t bag, + ICommand* command) : + bag_(bag), + command_(command) + { + } + + bool Execute() + { + try + { + return command_->Execute(); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What(); + return false; + } + catch (std::runtime_error& e) + { + LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what(); + return false; + } + catch (...) + { + LOG(ERROR) << "Native exception while processing a bag of tasks"; + return false; + } + } + + uint64_t GetBag() + { + return bag_; + } + }; + + + void BagOfTasksProcessor::SignalProgress(Task& task, + Bag& bag) + { + assert(bag.done_ < bag.size_); + + bag.done_ += 1; + + if (bag.done_ == bag.size_) + { + exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running); + bagFinished_.notify_all(); + } + } + + void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that) + { + while (that->continue_) + { + std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) + { + Task& task = *dynamic_cast<Task*>(obj.get()); + + { + boost::mutex::scoped_lock lock(that->mutex_); + + Bags::iterator bag = that->bags_.find(task.GetBag()); + assert(bag != that->bags_.end()); + assert(bag->second.done_ < bag->second.size_); + + if (bag->second.status_ != BagStatus_Running) + { + // Do not execute this task, as its parent bag of tasks + // has failed or is tagged as canceled + that->SignalProgress(task, bag->second); + continue; + } + } + + bool success = task.Execute(); + + { + boost::mutex::scoped_lock lock(that->mutex_); + + Bags::iterator bag = that->bags_.find(task.GetBag()); + assert(bag != that->bags_.end()); + + if (!success) + { + bag->second.status_ = BagStatus_Failed; + } + + that->SignalProgress(task, bag->second); + } + } + } + } + + + void BagOfTasksProcessor::Cancel(int64_t bag) + { + boost::mutex::scoped_lock lock(mutex_); + + Bags::iterator it = bags_.find(bag); + if (it != bags_.end()) + { + it->second.status_ = BagStatus_Canceled; + } + } + + + bool BagOfTasksProcessor::Join(int64_t bag) + { + boost::mutex::scoped_lock lock(mutex_); + + while (continue_) + { + ExitStatus::iterator it = exitStatus_.find(bag); + if (it == exitStatus_.end()) // The bag is still running + { + bagFinished_.wait(lock); + } + else + { + bool status = it->second; + exitStatus_.erase(it); + return status; + } + } + + return false; // The processor is stopping + } + + + float BagOfTasksProcessor::GetProgress(int64_t bag) + { + boost::mutex::scoped_lock lock(mutex_); + + Bags::const_iterator it = bags_.find(bag); + if (it == bags_.end()) + { + // The bag of tasks has finished + return 1.0f; + } + else + { + return (static_cast<float>(it->second.done_) / + static_cast<float>(it->second.size_)); + } + } + + + bool BagOfTasksProcessor::Handle::Join() + { + if (hasJoined_) + { + return status_; + } + else + { + status_ = that_.Join(bag_); + hasJoined_ = true; + return status_; + } + } + + + BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : + countBags_(0), + continue_(true) + { + if (countThreads == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + threads_.resize(countThreads); + + for (size_t i = 0; i < threads_.size(); i++) + { + threads_[i] = new boost::thread(Worker, this); + } + } + + + BagOfTasksProcessor::~BagOfTasksProcessor() + { + continue_ = false; + + bagFinished_.notify_all(); // Wakes up all the pending "Join()" + + for (size_t i = 0; i < threads_.size(); i++) + { + if (threads_[i]) + { + if (threads_[i]->joinable()) + { + threads_[i]->join(); + } + + delete threads_[i]; + threads_[i] = NULL; + } + } + } + + + BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks) + { + if (tasks.GetSize() == 0) + { + return new Handle(*this, 0, true); + } + + boost::mutex::scoped_lock lock(mutex_); + + uint64_t id = countBags_; + countBags_ += 1; + + Bag bag(tasks.GetSize()); + bags_[id] = bag; + + while (!tasks.IsEmpty()) + { + queue_.Enqueue(new Task(id, tasks.Pop())); + } + + return new Handle(*this, id, false); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,150 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "BagOfTasks.h" +#include "SharedMessageQueue.h" + +#include <stdint.h> +#include <map> + +namespace Orthanc +{ + class BagOfTasksProcessor : public boost::noncopyable + { + private: + enum BagStatus + { + BagStatus_Running, + BagStatus_Canceled, + BagStatus_Failed + }; + + + struct Bag + { + size_t size_; + size_t done_; + BagStatus status_; + + Bag() : + size_(0), + done_(0), + status_(BagStatus_Failed) + { + } + + explicit Bag(size_t size) : + size_(size), + done_(0), + status_(BagStatus_Running) + { + } + }; + + class Task; + + + typedef std::map<uint64_t, Bag> Bags; + typedef std::map<uint64_t, bool> ExitStatus; + + SharedMessageQueue queue_; + + boost::mutex mutex_; + uint64_t countBags_; + Bags bags_; + std::vector<boost::thread*> threads_; + ExitStatus exitStatus_; + bool continue_; + + boost::condition_variable bagFinished_; + + static void Worker(BagOfTasksProcessor* that); + + void Cancel(int64_t bag); + + bool Join(int64_t bag); + + float GetProgress(int64_t bag); + + void SignalProgress(Task& task, + Bag& bag); + + public: + class Handle : public boost::noncopyable + { + friend class BagOfTasksProcessor; + + private: + BagOfTasksProcessor& that_; + uint64_t bag_; + bool hasJoined_; + bool status_; + + Handle(BagOfTasksProcessor& that, + uint64_t bag, + bool empty) : + that_(that), + bag_(bag), + hasJoined_(empty) + { + } + + public: + ~Handle() + { + Join(); + } + + void Cancel() + { + that_.Cancel(bag_); + } + + bool Join(); + + float GetProgress() + { + return that_.GetProgress(bag_); + } + }; + + + explicit BagOfTasksProcessor(size_t countThreads); + + ~BagOfTasksProcessor(); + + Handle* Submit(BagOfTasks& tasks); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/ILockable.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class ILockable : public boost::noncopyable + { + friend class Locker; + + protected: + virtual void Lock() = 0; + + virtual void Unlock() = 0; + + public: + virtual ~ILockable() + { + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/Locker.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "ILockable.h" + +namespace Orthanc +{ + class Locker : public boost::noncopyable + { + private: + ILockable& lockable_; + + public: + Locker(ILockable& lockable) : lockable_(lockable) + { + lockable_.Lock(); + } + + virtual ~Locker() + { + lockable_.Unlock(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/Mutex.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,122 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "Mutex.h" + +#include "../OrthancException.h" + +#if defined(_WIN32) +#include <windows.h> +#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#include <pthread.h> +#else +#error Support your platform here +#endif + +namespace Orthanc +{ +#if defined (_WIN32) + + struct Mutex::PImpl + { + CRITICAL_SECTION criticalSection_; + }; + + Mutex::Mutex() + { + pimpl_ = new PImpl; + ::InitializeCriticalSection(&pimpl_->criticalSection_); + } + + Mutex::~Mutex() + { + ::DeleteCriticalSection(&pimpl_->criticalSection_); + delete pimpl_; + } + + void Mutex::Lock() + { + ::EnterCriticalSection(&pimpl_->criticalSection_); + } + + void Mutex::Unlock() + { + ::LeaveCriticalSection(&pimpl_->criticalSection_); + } + + +#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) + + struct Mutex::PImpl + { + pthread_mutex_t mutex_; + }; + + Mutex::Mutex() + { + pimpl_ = new PImpl; + + if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0) + { + delete pimpl_; + throw OrthancException(ErrorCode_InternalError); + } + } + + Mutex::~Mutex() + { + pthread_mutex_destroy(&pimpl_->mutex_); + delete pimpl_; + } + + void Mutex::Lock() + { + if (pthread_mutex_lock(&pimpl_->mutex_) != 0) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + void Mutex::Unlock() + { + if (pthread_mutex_unlock(&pimpl_->mutex_) != 0) + { + throw OrthancException(ErrorCode_InternalError); + } + } + +#else +#error Support your plateform here +#endif +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/Mutex.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "ILockable.h" + +namespace Orthanc +{ + class Mutex : public ILockable + { + private: + struct PImpl; + + PImpl *pimpl_; + + protected: + virtual void Lock(); + + virtual void Unlock(); + + public: + Mutex(); + + ~Mutex(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,126 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "ReaderWriterLock.h" + +#include <boost/thread/shared_mutex.hpp> + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation + // modules. + + class ReaderLockable : public ILockable + { + private: + boost::shared_mutex& lock_; + + protected: + virtual void Lock() + { + lock_.lock_shared(); + } + + virtual void Unlock() + { + lock_.unlock_shared(); + } + + public: + explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock) + { + } + }; + + + class WriterLockable : public ILockable + { + private: + boost::shared_mutex& lock_; + + protected: + virtual void Lock() + { + lock_.lock(); + } + + virtual void Unlock() + { + lock_.unlock(); + } + + public: + explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock) + { + } + }; + } + + struct ReaderWriterLock::PImpl + { + boost::shared_mutex lock_; + ReaderLockable reader_; + WriterLockable writer_; + + PImpl() : reader_(lock_), writer_(lock_) + { + } + }; + + + ReaderWriterLock::ReaderWriterLock() + { + pimpl_ = new PImpl; + } + + + ReaderWriterLock::~ReaderWriterLock() + { + delete pimpl_; + } + + + ILockable& ReaderWriterLock::ForReader() + { + return pimpl_->reader_; + } + + + ILockable& ReaderWriterLock::ForWriter() + { + return pimpl_->writer_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,58 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "ILockable.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class ReaderWriterLock : public boost::noncopyable + { + private: + struct PImpl; + + PImpl *pimpl_; + + public: + ReaderWriterLock(); + + virtual ~ReaderWriterLock(); + + ILockable& ForReader(); + + ILockable& ForWriter(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/Semaphore.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "Semaphore.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + Semaphore::Semaphore(unsigned int count) : count_(count) + { + } + + void Semaphore::Release() + { + boost::mutex::scoped_lock lock(mutex_); + + count_++; + condition_.notify_one(); + } + + void Semaphore::Acquire() + { + boost::mutex::scoped_lock lock(mutex_); + + while (count_ == 0) + { + condition_.wait(lock); + } + + count_++; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/Multithreading/Semaphore.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> +#include <boost/thread.hpp> + +namespace Orthanc +{ + class Semaphore : public boost::noncopyable + { + private: + unsigned int count_; + boost::mutex mutex_; + boost::condition_variable condition_; + + public: + explicit Semaphore(unsigned int count); + + void Release(); + + void Acquire(); + + class Locker : public boost::noncopyable + { + private: + Semaphore& that_; + + public: + explicit Locker(Semaphore& that) : + that_(that) + { + that_.Acquire(); + } + + ~Locker() + { + that_.Release(); + } + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "CallSystemCommand.h" + +#include "../../Core/Logging.h" +#include "../../Core/Toolbox.h" +#include "../../Core/TemporaryFile.h" + +namespace Orthanc +{ + CallSystemCommand::CallSystemCommand(ServerContext& context, + const std::string& command, + const std::vector<std::string>& arguments) : + context_(context), + command_(command), + arguments_(arguments) + { + } + + bool CallSystemCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Calling system command " << command_ << " on instance " << *it; + + try + { + std::string dicom; + context_.ReadDicom(dicom, *it); + + TemporaryFile tmp; + tmp.Write(dicom); + + std::vector<std::string> args = arguments_; + args.push_back(tmp.GetPath()); + + SystemToolbox::ExecuteSystemCommand(command_, args); + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to call system command " << command_ + << " on instance " << *it << " in a Lua script: " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class CallSystemCommand : public IServerCommand + { + private: + ServerContext& context_; + std::string command_; + std::vector<std::string> arguments_; + + public: + CallSystemCommand(ServerContext& context, + const std::string& command, + const std::vector<std::string>& arguments); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,62 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "DeleteInstanceCommand.h" + +#include "../../Core/Logging.h" + +namespace Orthanc +{ + bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Deleting instance " << *it; + + try + { + Json::Value tmp; + context_.DeleteResource(tmp, *it, ResourceType_Instance); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class DeleteInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + + public: + DeleteInstanceCommand(ServerContext& context) : context_(context) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/IServerCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include <list> +#include <string> +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IServerCommand : public boost::noncopyable + { + public: + typedef std::list<std::string> ListOfStrings; + + virtual ~IServerCommand() + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,124 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ModifyInstanceCommand.h" + +#include "../../Core/Logging.h" + +namespace Orthanc +{ + ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, + RequestOrigin origin, + DicomModification* modification) : + context_(context), + origin_(origin), + modification_(modification) + { + modification_->SetAllowManualIdentifiers(true); + + if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) + { + modification_->SetLevel(ResourceType_Patient); + } + else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + modification_->SetLevel(ResourceType_Study); + } + else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + modification_->SetLevel(ResourceType_Series); + } + else + { + modification_->SetLevel(ResourceType_Instance); + } + + if (origin_ != RequestOrigin_Lua) + { + // TODO If issued from HTTP, "remoteIp" and "username" must be provided + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + ModifyInstanceCommand::~ModifyInstanceCommand() + { + if (modification_) + { + delete modification_; + } + } + + + bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Modifying resource " << *it; + + try + { + std::auto_ptr<ParsedDicomFile> modified; + + { + ServerContext::DicomCacheLocker lock(context_, *it); + modified.reset(lock.GetDicom().Clone(true)); + } + + modification_->Apply(*modified); + + DicomInstanceToStore toStore; + assert(origin_ == RequestOrigin_Lua); + toStore.SetLuaOrigin(); + toStore.SetParsedDicomFile(*modified); + // TODO other metadata + toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); + + std::string modifiedId; + context_.Store(modifiedId, toStore); + + // Only chain with other commands if this command succeeds + outputs.push_back(modifiedId); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" +#include "../../Core/DicomParsing/DicomModification.h" + +namespace Orthanc +{ + class ModifyInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + RequestOrigin origin_; + DicomModification* modification_; + + public: + ModifyInstanceCommand(ServerContext& context, + RequestOrigin origin, + DicomModification* modification); // takes the ownership + + virtual ~ModifyInstanceCommand(); + + const DicomModification& GetModification() const + { + return *modification_; + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,188 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "../PrecompiledHeaders.h" +#include "ReusableDicomUserConnection.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +namespace Orthanc +{ + static boost::posix_time::ptime Now() + { + return boost::posix_time::microsec_clock::local_time(); + } + + void ReusableDicomUserConnection::Open(const std::string& localAet, + const RemoteModalityParameters& remote) + { + if (connection_ != NULL && + connection_->GetLocalApplicationEntityTitle() == localAet && + connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() && + connection_->GetRemoteHost() == remote.GetHost() && + connection_->GetRemotePort() == remote.GetPort() && + connection_->GetRemoteManufacturer() == remote.GetManufacturer()) + { + // The current connection can be reused + LOG(INFO) << "Reusing the previous SCU connection"; + return; + } + + Close(); + + connection_ = new DicomUserConnection(); + connection_->SetLocalApplicationEntityTitle(localAet); + connection_->SetRemoteModality(remote); + connection_->Open(); + } + + void ReusableDicomUserConnection::Close() + { + if (connection_ != NULL) + { + delete connection_; + connection_ = NULL; + } + } + + void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that) + { + for (;;) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + if (!that->continue_) + { + //LOG(INFO) << "Finishing the thread watching the global SCU connection"; + return; + } + + { + boost::mutex::scoped_lock lock(that->mutex_); + if (that->connection_ != NULL && + Now() >= that->lastUse_ + that->timeBeforeClose_) + { + LOG(INFO) << "Closing the global SCU connection after timeout"; + that->Close(); + } + } + } + } + + + ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that, + const std::string& localAet, + const RemoteModalityParameters& remote) : + ::Orthanc::Locker(that) + { + that.Open(localAet, remote); + connection_ = that.connection_; + } + + + DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection() + { + if (connection_ == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *connection_; + } + + ReusableDicomUserConnection::ReusableDicomUserConnection() : + connection_(NULL), + timeBeforeClose_(boost::posix_time::seconds(5)) // By default, close connection after 5 seconds + { + lastUse_ = Now(); + continue_ = true; + closeThread_ = boost::thread(CloseThread, this); + } + + ReusableDicomUserConnection::~ReusableDicomUserConnection() + { + if (continue_) + { + LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!"; + Finalize(); + } + } + + void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms) + { + boost::mutex::scoped_lock lock(mutex_); + + if (ms == 0) + { + ms = 1; + } + + timeBeforeClose_ = boost::posix_time::milliseconds(ms); + } + + void ReusableDicomUserConnection::Lock() + { + mutex_.lock(); + } + + void ReusableDicomUserConnection::Unlock() + { + if (connection_ != NULL && + connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp) + { + // "storescp" from DCMTK has problems when reusing a + // connection. Always close. + Close(); + } + + lastUse_ = Now(); + mutex_.unlock(); + } + + + void ReusableDicomUserConnection::Finalize() + { + if (continue_) + { + continue_ = false; + + if (closeThread_.joinable()) + { + closeThread_.join(); + } + + Close(); + } + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,89 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "DicomUserConnection.h" +#include "../../Core/MultiThreading/Locker.h" + +#include <boost/thread.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace Orthanc +{ + class ReusableDicomUserConnection : public ILockable + { + private: + boost::mutex mutex_; + DicomUserConnection* connection_; + bool continue_; + boost::posix_time::time_duration timeBeforeClose_; + boost::posix_time::ptime lastUse_; + boost::thread closeThread_; + + void Open(const std::string& localAet, + const RemoteModalityParameters& remote); + + void Close(); + + static void CloseThread(ReusableDicomUserConnection* that); + + protected: + virtual void Lock(); + + virtual void Unlock(); + + public: + class Locker : public ::Orthanc::Locker + { + private: + DicomUserConnection* connection_; + + public: + Locker(ReusableDicomUserConnection& that, + const std::string& localAet, + const RemoteModalityParameters& remote); + + DicomUserConnection& GetConnection(); + }; + + ReusableDicomUserConnection(); + + virtual ~ReusableDicomUserConnection(); + + void SetMillisecondsBeforeClose(uint64_t ms); + + void Finalize(); + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ServerCommandInstance.h" + +#include "../../Core/OrthancException.h" + +namespace Orthanc +{ + bool ServerCommandInstance::Execute(IListener& listener) + { + ListOfStrings outputs; + + bool success = false; + + try + { + if (command_->Apply(outputs, inputs_)) + { + success = true; + } + } + catch (OrthancException&) + { + } + + if (!success) + { + listener.SignalFailure(jobId_); + return true; + } + + for (std::list<ServerCommandInstance*>::iterator + it = next_.begin(); it != next_.end(); ++it) + { + for (ListOfStrings::const_iterator + output = outputs.begin(); output != outputs.end(); ++output) + { + (*it)->AddInput(*output); + } + } + + listener.SignalSuccess(jobId_); + return true; + } + + + ServerCommandInstance::ServerCommandInstance(IServerCommand *command, + const std::string& jobId) : + command_(command), + jobId_(jobId), + connectedToSink_(false) + { + if (command_ == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ServerCommandInstance::~ServerCommandInstance() + { + if (command_ != NULL) + { + delete command_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,105 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../Core/IDynamicObject.h" +#include "IServerCommand.h" + +namespace Orthanc +{ + class ServerCommandInstance : public IDynamicObject + { + friend class ServerScheduler; + + public: + class IListener + { + public: + virtual ~IListener() + { + } + + virtual void SignalSuccess(const std::string& jobId) = 0; + + virtual void SignalFailure(const std::string& jobId) = 0; + }; + + private: + typedef IServerCommand::ListOfStrings ListOfStrings; + + IServerCommand *command_; + std::string jobId_; + ListOfStrings inputs_; + std::list<ServerCommandInstance*> next_; + bool connectedToSink_; + + bool Execute(IListener& listener); + + public: + ServerCommandInstance(IServerCommand *command, + const std::string& jobId); + + virtual ~ServerCommandInstance(); + + const std::string& GetJobId() const + { + return jobId_; + } + + void AddInput(const std::string& input) + { + inputs_.push_back(input); + } + + void ConnectOutput(ServerCommandInstance& next) + { + next_.push_back(&next); + } + + void SetConnectedToSink(bool connected = true) + { + connectedToSink_ = connected; + } + + bool IsConnectedToSink() const + { + return connectedToSink_; + } + + const std::list<ServerCommandInstance*>& GetNextCommands() const + { + return next_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerJob.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,147 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ServerJob.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" + +namespace Orthanc +{ + void ServerJob::CheckOrdering() + { + std::map<ServerCommandInstance*, unsigned int> index; + + unsigned int count = 0; + for (std::list<ServerCommandInstance*>::const_iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + index[*it] = count++; + } + + for (std::list<ServerCommandInstance*>::const_iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands(); + + for (std::list<ServerCommandInstance*>::const_iterator + next = nextCommands.begin(); next != nextCommands.end(); ++next) + { + if (index.find(*next) == index.end() || + index[*next] <= index[*it]) + { + // You must reorder your calls to "ServerJob::AddCommand" + throw OrthancException(ErrorCode_BadJobOrdering); + } + } + } + } + + + size_t ServerJob::Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener) + { + if (submitted_) + { + // This job has already been submitted + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + CheckOrdering(); + + size_t size = filters_.size(); + + for (std::list<ServerCommandInstance*>::iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + target.Enqueue(*it); + } + + filters_.clear(); + submitted_ = true; + + return size; + } + + + ServerJob::ServerJob() : + jobId_(Toolbox::GenerateUuid()), + submitted_(false), + description_("no description") + { + } + + + ServerJob::~ServerJob() + { + for (std::list<ServerCommandInstance*>::iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + delete *it; + } + + for (std::list<IDynamicObject*>::iterator + it = payloads_.begin(); it != payloads_.end(); ++it) + { + delete *it; + } + } + + + ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + filters_.push_back(new ServerCommandInstance(filter, jobId_)); + + return *filters_.back(); + } + + + IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + payloads_.push_back(payload); + + return *filters_.back(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerJob.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,83 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "ServerCommandInstance.h" +#include "../../Core/MultiThreading/SharedMessageQueue.h" + +namespace Orthanc +{ + class ServerJob + { + friend class ServerScheduler; + + private: + std::list<ServerCommandInstance*> filters_; + std::list<IDynamicObject*> payloads_; + std::string jobId_; + bool submitted_; + std::string description_; + + void CheckOrdering(); + + size_t Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener); + + public: + ServerJob(); + + ~ServerJob(); + + const std::string& GetId() const + { + return jobId_; + } + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + ServerCommandInstance& AddCommand(IServerCommand* filter); + + // Take the ownership of a payload to a job. This payload will be + // automatically freed when the job succeeds or fails. + IDynamicObject& AddPayload(IDynamicObject* payload); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerScheduler.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,359 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "ServerScheduler.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Logging.h" + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class Sink : public IServerCommand + { + private: + ListOfStrings& target_; + + public: + explicit Sink(ListOfStrings& target) : target_(target) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + target_.push_back(*it); + } + + return true; + } + }; + } + + + ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) + { + Jobs::iterator info = jobs_.find(jobId); + + if (info == jobs_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return info->second; + } + + + void ServerScheduler::SignalSuccess(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.success_++; + + assert(info.failures_ == 0); + + if (info.success_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Success; + watchedJobFinished_.notify_all(); + } + + LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::SignalFailure(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.failures_++; + + if (info.success_ + info.failures_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Failure; + watchedJobFinished_.notify_all(); + } + + LOG(ERROR) << "Job has failed (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::Worker(ServerScheduler* that) + { + static const int32_t TIMEOUT = 100; + + LOG(WARNING) << "The server scheduler has started"; + + while (!that->finish_) + { + std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT)); + if (object.get() != NULL) + { + ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object); + + // Skip the execution of this filter if its parent job has + // previously failed. + bool jobHasFailed; + { + boost::mutex::scoped_lock lock(that->mutex_); + JobInfo& info = that->GetJobInfo(filter.GetJobId()); + jobHasFailed = (info.failures_ > 0 || info.cancel_); + } + + if (jobHasFailed) + { + that->SignalFailure(filter.GetJobId()); + } + else + { + filter.Execute(*that); + } + } + } + } + + + void ServerScheduler::SubmitInternal(ServerJob& job, + bool watched) + { + availableJob_.Acquire(); + + boost::mutex::scoped_lock lock(mutex_); + + JobInfo info; + info.size_ = job.Submit(queue_, *this); + info.cancel_ = false; + info.success_ = 0; + info.failures_ = 0; + info.description_ = job.GetDescription(); + info.watched_ = watched; + + assert(info.size_ > 0); + + if (watched) + { + watchedJobStatus_[job.GetId()] = JobStatus_Running; + } + + jobs_[job.GetId()] = info; + + LOG(INFO) << "New job submitted (" << job.description_ << ")"; + } + + + ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) + { + if (maxJobs == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + finish_ = false; + worker_ = boost::thread(Worker, this); + } + + + ServerScheduler::~ServerScheduler() + { + if (!finish_) + { + LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } + } + + + void ServerScheduler::Stop() + { + if (!finish_) + { + finish_ = true; + + if (worker_.joinable()) + { + worker_.join(); + } + } + } + + + void ServerScheduler::Submit(ServerJob& job) + { + if (job.filters_.empty()) + { + return; + } + + SubmitInternal(job, false); + } + + + bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, + ServerJob& job) + { + std::string jobId = job.GetId(); + + outputs.clear(); + + if (job.filters_.empty()) + { + return true; + } + + // Add a sink filter to collect all the results of the filters + // that have no next filter. + ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); + + for (std::list<ServerCommandInstance*>::iterator + it = job.filters_.begin(); it != job.filters_.end(); ++it) + { + if ((*it) != &sink && + (*it)->IsConnectedToSink()) + { + (*it)->ConnectOutput(sink); + } + } + + // Submit the job + SubmitInternal(job, true); + + // Wait for the job to complete (either success or failure) + JobStatus status; + + { + boost::mutex::scoped_lock lock(mutex_); + + assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); + + while (watchedJobStatus_[jobId] == JobStatus_Running) + { + watchedJobFinished_.wait(lock); + } + + status = watchedJobStatus_[jobId]; + watchedJobStatus_.erase(jobId); + } + + return (status == JobStatus_Success); + } + + + bool ServerScheduler::SubmitAndWait(ServerJob& job) + { + ListOfStrings ignoredSink; + return SubmitAndWait(ignoredSink, job); + } + + + bool ServerScheduler::IsRunning(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + return jobs_.find(jobId) != jobs_.end(); + } + + + void ServerScheduler::Cancel(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job != jobs_.end()) + { + job->second.cancel_ = true; + LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; + } + } + + + float ServerScheduler::GetProgress(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job == jobs_.end() || + job->second.size_ == 0 /* should never happen */) + { + // This job is not running + return 1; + } + + if (job->second.failures_ != 0) + { + return 1; + } + + if (job->second.size_ == 1) + { + return static_cast<float>(job->second.success_); + } + + return (static_cast<float>(job->second.success_) / + static_cast<float>(job->second.size_ - 1)); + } + + + void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) + { + boost::mutex::scoped_lock lock(mutex_); + + jobs.clear(); + + for (Jobs::const_iterator + it = jobs_.begin(); it != jobs_.end(); ++it) + { + jobs.push_back(it->first); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/ServerScheduler.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,123 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "ServerJob.h" + +#include "../../Core/MultiThreading/Semaphore.h" + +namespace Orthanc +{ + class ServerScheduler : public ServerCommandInstance::IListener + { + private: + struct JobInfo + { + bool watched_; + bool cancel_; + size_t size_; + size_t success_; + size_t failures_; + std::string description_; + }; + + enum JobStatus + { + JobStatus_Running = 1, + JobStatus_Success = 2, + JobStatus_Failure = 3 + }; + + typedef IServerCommand::ListOfStrings ListOfStrings; + typedef std::map<std::string, JobInfo> Jobs; + + boost::mutex mutex_; + boost::condition_variable watchedJobFinished_; + Jobs jobs_; + SharedMessageQueue queue_; + bool finish_; + boost::thread worker_; + std::map<std::string, JobStatus> watchedJobStatus_; + Semaphore availableJob_; + + JobInfo& GetJobInfo(const std::string& jobId); + + virtual void SignalSuccess(const std::string& jobId); + + virtual void SignalFailure(const std::string& jobId); + + static void Worker(ServerScheduler* that); + + void SubmitInternal(ServerJob& job, + bool watched); + + public: + explicit ServerScheduler(unsigned int maxjobs); + + ~ServerScheduler(); + + void Stop(); + + void Submit(ServerJob& job); + + bool SubmitAndWait(ListOfStrings& outputs, + ServerJob& job); + + bool SubmitAndWait(ServerJob& job); + + bool IsRunning(const std::string& jobId); + + void Cancel(const std::string& jobId); + + // Returns a number between 0 and 1 + float GetProgress(const std::string& jobId); + + bool IsRunning(const ServerJob& job) + { + return IsRunning(job.GetId()); + } + + void Cancel(const ServerJob& job) + { + Cancel(job.GetId()); + } + + float GetProgress(const ServerJob& job) + { + return GetProgress(job.GetId()); + } + + void GetListOfJobs(ListOfStrings& jobs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,92 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "StorePeerCommand.h" + +#include "../../Core/Logging.h" +#include "../../Core/HttpClient.h" + +namespace Orthanc +{ + StorePeerCommand::StorePeerCommand(ServerContext& context, + const WebServiceParameters& peer, + bool ignoreExceptions) : + context_(context), + peer_(peer), + ignoreExceptions_(ignoreExceptions) + { + } + + bool StorePeerCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + // Configure the HTTP client + HttpClient client(peer_, "instances"); + client.SetMethod(HttpMethod_Post); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to peer \"" + << peer_.GetUrl() << "\""; + + try + { + context_.ReadDicom(client.GetBody(), *it); + + std::string answer; + if (!client.Apply(answer)) + { + LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; + throw OrthancException(ErrorCode_NetworkProtocol); + } + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance " + << *it << ", peer " << peer_.GetUrl() << "): " << e.What(); + + if (!ignoreExceptions_) + { + throw; + } + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" +#include "../OrthancInitialization.h" + +namespace Orthanc +{ + class StorePeerCommand : public IServerCommand + { + private: + ServerContext& context_; + WebServiceParameters peer_; + bool ignoreExceptions_; + + public: + StorePeerCommand(ServerContext& context, + const WebServiceParameters& peer, + bool ignoreExceptions); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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" +#include "StoreScuCommand.h" + +#include "../../Core/Logging.h" + +namespace Orthanc +{ + StoreScuCommand::StoreScuCommand(ServerContext& context, + const std::string& localAet, + const RemoteModalityParameters& modality, + bool ignoreExceptions) : + context_(context), + modality_(modality), + ignoreExceptions_(ignoreExceptions), + localAet_(localAet), + moveOriginatorID_(0) + { + } + + + void StoreScuCommand::SetMoveOriginator(const std::string& aet, + uint16_t id) + { + moveOriginatorAET_ = aet; + moveOriginatorID_ = id; + } + + + bool StoreScuCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to modality \"" + << modality_.GetApplicationEntityTitle() << "\""; + + try + { + std::string dicom; + context_.ReadDicom(dicom, *it); + + locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_); + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + // Ignore transmission errors (e.g. if the remote modality is + // powered off) + LOG(ERROR) << "Unable to forward to a modality in (instance " + << *it << "): " << e.What(); + + if (!ignoreExceptions_) + { + throw; + } + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.h Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,63 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class StoreScuCommand : public IServerCommand + { + private: + ServerContext& context_; + RemoteModalityParameters modality_; + bool ignoreExceptions_; + std::string localAet_; + std::string moveOriginatorAET_; + uint16_t moveOriginatorID_; + + public: + StoreScuCommand(ServerContext& context, + const std::string& localAet, + const RemoteModalityParameters& modality, + bool ignoreExceptions); + + void SetMoveOriginator(const std::string& aet, + uint16_t id); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/ImplementationNotes/JobsEngineStates.dot Mon Jun 11 16:30:13 2018 +0200 @@ -0,0 +1,28 @@ +// dot -Tpdf JobsEngineStates.dot -o JobsEngineStates.pdf + +digraph G +{ + rankdir="LR"; + init [shape=point]; + failure, success [shape=doublecircle]; + + // Internal transitions + init -> pending; + pending -> running; + running -> success; + running -> failure; + running -> retry; + retry -> pending [label="timeout"]; + + // External actions + failure -> pending [label="Resubmit()" fontcolor="red"]; + paused -> pending [label="Resume()" fontcolor="red"]; + pending -> paused [label="Pause()" fontcolor="red"]; + retry -> paused [label="Pause()" fontcolor="red"]; + running -> paused [label="Pause()" fontcolor="red"]; + + paused -> failure [label="Cancel()" fontcolor="red"]; + pending -> failure [label="Cancel()" fontcolor="red"]; + retry -> failure [label="Cancel()" fontcolor="red"]; + running -> failure [label="Cancel()" fontcolor="red"]; +}
--- a/UnitTestsSources/MultiThreadingTests.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -34,18 +34,166 @@ #include "PrecompiledHeadersUnitTests.h" #include "gtest/gtest.h" -#include "../OrthancServer/Scheduler/ServerScheduler.h" +#include "../Core/FileStorage/MemoryStorageArea.h" +#include "../Core/JobsEngine/JobsEngine.h" +#include "../Core/Logging.h" +#include "../Core/MultiThreading/SharedMessageQueue.h" #include "../Core/OrthancException.h" +#include "../Core/SerializationToolbox.h" #include "../Core/SystemToolbox.h" #include "../Core/Toolbox.h" -#include "../Core/MultiThreading/Locker.h" -#include "../Core/MultiThreading/Mutex.h" -#include "../Core/MultiThreading/ReaderWriterLock.h" +#include "../OrthancServer/DatabaseWrapper.h" +#include "../OrthancServer/ServerContext.h" +#include "../OrthancServer/ServerJobs/LuaJobManager.h" +#include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h" + +#include "../Core/JobsEngine/Operations/JobOperationValues.h" +#include "../Core/JobsEngine/Operations/NullOperationValue.h" +#include "../Core/JobsEngine/Operations/StringOperationValue.h" +#include "../OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h" + +#include "../Core/JobsEngine/Operations/LogJobOperation.h" +#include "../OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h" +#include "../OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h" +#include "../OrthancServer/ServerJobs/Operations/StorePeerOperation.h" +#include "../OrthancServer/ServerJobs/Operations/StoreScuOperation.h" +#include "../OrthancServer/ServerJobs/Operations/SystemCallOperation.h" + +#include "../OrthancServer/ServerJobs/ArchiveJob.h" +#include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h" +#include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h" +#include "../OrthancServer/ServerJobs/ResourceModificationJob.h" + using namespace Orthanc; namespace { + class DummyJob : public IJob + { + private: + bool fails_; + unsigned int count_; + unsigned int steps_; + + public: + DummyJob() : + fails_(false), + count_(0), + steps_(4) + { + } + + explicit DummyJob(bool fails) : + fails_(fails), + count_(0), + steps_(4) + { + } + + virtual void Start() + { + } + + virtual void SignalResubmit() + { + } + + virtual JobStepResult ExecuteStep() + { + if (fails_) + { + return JobStepResult::Failure(ErrorCode_ParameterOutOfRange); + } + else if (count_ == steps_ - 1) + { + return JobStepResult::Success(); + } + else + { + count_++; + return JobStepResult::Continue(); + } + } + + virtual void ReleaseResources() + { + } + + virtual float GetProgress() + { + return static_cast<float>(count_) / static_cast<float>(steps_ - 1); + } + + virtual void GetJobType(std::string& type) + { + type = "DummyJob"; + } + + virtual bool Serialize(Json::Value& value) + { + value = Json::objectValue; + value["Type"] = "DummyJob"; + return true; + } + + virtual void GetPublicContent(Json::Value& value) + { + value["hello"] = "world"; + } + }; + + + class DummyInstancesJob : public SetOfInstancesJob + { + protected: + virtual bool HandleInstance(const std::string& instance) + { + return (instance != "nope"); + } + + public: + DummyInstancesJob() + { + } + + DummyInstancesJob(const Json::Value& value) : + SetOfInstancesJob(value) + { + } + + virtual void ReleaseResources() + { + } + + virtual void GetJobType(std::string& s) + { + s = "DummyInstancesJob"; + } + }; + + + class DummyUnserializer : public GenericJobUnserializer + { + public: + virtual IJob* UnserializeJob(const Json::Value& value) + { + if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob") + { + return new DummyInstancesJob(value); + } + else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob") + { + return new DummyJob; + } + else + { + return GenericJobUnserializer::UnserializeJob(value); + } + } + }; + + class DynamicInteger : public IDynamicObject { private: @@ -106,156 +254,1240 @@ } -TEST(MultiThreading, Mutex) + + +static bool CheckState(JobsRegistry& registry, + const std::string& id, + JobState state) { - Mutex mutex; - Locker locker(mutex); + JobState s; + if (registry.GetState(s, id)) + { + return state == s; + } + else + { + return false; + } } -TEST(MultiThreading, ReaderWriterLock) +static bool CheckErrorCode(JobsRegistry& registry, + const std::string& id, + ErrorCode code) { - ReaderWriterLock lock; - + JobInfo s; + if (registry.GetJobInfo(s, id)) { - Locker locker1(lock.ForReader()); - Locker locker2(lock.ForReader()); + return code == s.GetStatus().GetErrorCode(); } - + else { - Locker locker3(lock.ForWriter()); + return false; } } +TEST(JobsRegistry, Priority) +{ + JobsRegistry registry; -#include "../Core/DicomNetworking/ReusableDicomUserConnection.h" + std::string i1, i2, i3, i4; + registry.Submit(i1, new DummyJob(), 10); + registry.Submit(i2, new DummyJob(), 30); + registry.Submit(i3, new DummyJob(), 20); + registry.Submit(i4, new DummyJob(), 5); + + registry.SetMaxCompletedJobs(2); + + std::set<std::string> id; + registry.ListJobs(id); + + ASSERT_EQ(4u, id.size()); + ASSERT_TRUE(id.find(i1) != id.end()); + ASSERT_TRUE(id.find(i2) != id.end()); + ASSERT_TRUE(id.find(i3) != id.end()); + ASSERT_TRUE(id.find(i4) != id.end()); + + ASSERT_TRUE(CheckState(registry, i2, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(30, job.GetPriority()); + ASSERT_EQ(i2, job.GetId()); + + ASSERT_TRUE(CheckState(registry, i2, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i2, JobState_Failure)); + ASSERT_TRUE(CheckState(registry, i3, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(20, job.GetPriority()); + ASSERT_EQ(i3, job.GetId()); + + job.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, i3, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i3, JobState_Success)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(10, job.GetPriority()); + ASSERT_EQ(i1, job.GetId()); + } + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(5, job.GetPriority()); + ASSERT_EQ(i4, job.GetId()); + } + + { + JobsRegistry::RunningJob job(registry, 1); + ASSERT_FALSE(job.IsValid()); + } + + JobState s; + ASSERT_TRUE(registry.GetState(s, i1)); + ASSERT_FALSE(registry.GetState(s, i2)); // Removed because oldest + ASSERT_FALSE(registry.GetState(s, i3)); // Removed because second oldest + ASSERT_TRUE(registry.GetState(s, i4)); + + registry.SetMaxCompletedJobs(1); // (*) + ASSERT_FALSE(registry.GetState(s, i1)); // Just discarded by (*) + ASSERT_TRUE(registry.GetState(s, i4)); +} + + +TEST(JobsRegistry, Simultaneous) +{ + JobsRegistry registry; + + std::string i1, i2; + registry.Submit(i1, new DummyJob(), 20); + registry.Submit(i2, new DummyJob(), 10); -TEST(ReusableDicomUserConnection, DISABLED_Basic) + ASSERT_TRUE(CheckState(registry, i1, JobState_Pending)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Pending)); + + { + JobsRegistry::RunningJob job1(registry, 0); + JobsRegistry::RunningJob job2(registry, 0); + + ASSERT_TRUE(job1.IsValid()); + ASSERT_TRUE(job2.IsValid()); + + job1.MarkFailure(); + job2.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, i1, JobState_Running)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i1, JobState_Failure)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Success)); +} + + +TEST(JobsRegistry, Resubmit) { - ReusableDicomUserConnection c; - c.SetMillisecondsBeforeClose(200); - printf("START\n"); fflush(stdout); + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkFailure(); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, Retry) +{ + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkRetry(0); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.ScheduleRetries(); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); { - RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic); - ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote); - lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281"); + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, PausePending) +{ + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); +} + + +TEST(JobsRegistry, PauseRunning) +{ + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + registry.Resubmit(id); + job.MarkPause(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, PauseRetry) +{ + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkRetry(0); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); } - printf("**\n"); fflush(stdout); - SystemToolbox::USleep(1000000); - printf("**\n"); fflush(stdout); + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); { - RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic); - ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote); - lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277"); + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, Cancel) +{ + JobsRegistry registry; + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_FALSE(registry.Cancel("nope")); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); } - SystemToolbox::ServerBarrier(); - printf("DONE\n"); fflush(stdout); + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + registry.Submit(id, new DummyJob(), 10); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + job.MarkCanceled(); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Pause(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + job.MarkRetry(500); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); } -class Tutu : public IServerCommand +TEST(JobsEngine, SubmitAndWait) +{ + JobsEngine engine; + engine.SetThreadSleep(10); + engine.SetWorkersCount(3); + engine.Start(); + + ASSERT_TRUE(engine.GetRegistry().SubmitAndWait(new DummyJob(), rand() % 10)); + ASSERT_FALSE(engine.GetRegistry().SubmitAndWait(new DummyJob(true), rand() % 10)); + + engine.Stop(); +} + + +TEST(JobsEngine, DISABLED_SequenceOfOperationsJob) { -private: - int factor_; + JobsEngine engine; + engine.SetThreadSleep(10); + engine.SetWorkersCount(3); + engine.Start(); + + std::string id; + SequenceOfOperationsJob* job = NULL; + + { + std::auto_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob); + job = a.get(); + engine.GetRegistry().Submit(id, a.release(), 0); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(500)); + + { + SequenceOfOperationsJob::Lock lock(*job); + size_t i = lock.AddOperation(new LogJobOperation); + size_t j = lock.AddOperation(new LogJobOperation); + size_t k = lock.AddOperation(new LogJobOperation); + lock.AddInput(i, StringOperationValue("Hello")); + lock.AddInput(i, StringOperationValue("World")); + lock.Connect(i, j); + lock.Connect(j, k); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(2000)); -public: - Tutu(int f) : factor_(f) + engine.Stop(); + +} + + +TEST(JobsEngine, DISABLED_Lua) +{ + JobsEngine engine; + engine.SetThreadSleep(10); + engine.SetWorkersCount(2); + engine.Start(); + + LuaJobManager lua; + lua.SetMaxOperationsPerJob(5); + lua.SetTrailingOperationTimeout(200); + + for (size_t i = 0; i < 30; i++) { + boost::this_thread::sleep(boost::posix_time::milliseconds(150)); + + LuaJobManager::Lock lock(lua, engine); + size_t a = lock.AddLogOperation(); + size_t b = lock.AddLogOperation(); + size_t c = lock.AddSystemCallOperation("echo"); + lock.AddStringInput(a, boost::lexical_cast<std::string>(i)); + lock.AddNullInput(a); + lock.Connect(a, b); + lock.Connect(a, c); } - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - int a = boost::lexical_cast<int>(*it); - int b = factor_ * a; + boost::this_thread::sleep(boost::posix_time::milliseconds(2000)); + + engine.Stop(); +} + - printf("%d * %d = %d\n", a, factor_, b); - - //if (a == 84) { printf("BREAK\n"); return false; } +static bool CheckSameJson(const Json::Value& a, + const Json::Value& b) +{ + std::string s = a.toStyledString(); + std::string t = b.toStyledString(); - outputs.push_back(boost::lexical_cast<std::string>(b)); - } - - SystemToolbox::USleep(30000); - + if (s == t) + { return true; } -}; + else + { + LOG(ERROR) << "Expected serialization: " << s; + LOG(ERROR) << "Actual serialization: " << t; + return false; + } +} + + +static bool CheckIdempotentSerialization(IJobUnserializer& unserializer, + IJob& job) +{ + Json::Value a = 42; + + if (!job.Serialize(a)) + { + return false; + } + else + { + std::auto_ptr<IJob> unserialized(unserializer.UnserializeJob(a)); + + Json::Value b = 43; + if (unserialized->Serialize(b)) + { + return CheckSameJson(a, b); + } + else + { + return false; + } + } +} + + +static bool CheckIdempotentSerialization(IJobUnserializer& unserializer, + IJobOperation& operation) +{ + Json::Value a = 42; + operation.Serialize(a); + + std::auto_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a)); + + Json::Value b = 43; + unserialized->Serialize(b); + + return CheckSameJson(a, b); +} + + +static bool CheckIdempotentSerialization(IJobUnserializer& unserializer, + JobOperationValue& value) +{ + Json::Value a = 42; + value.Serialize(a); + + std::auto_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a)); + + Json::Value b = 43; + unserialized->Serialize(b); + + return CheckSameJson(a, b); +} -static void Tata(ServerScheduler* s, ServerJob* j, bool* done) +TEST(JobsSerialization, BadFileFormat) { - typedef IServerCommand::ListOfStrings ListOfStrings; + GenericJobUnserializer unserializer; + + Json::Value s; + + s = Json::objectValue; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + s = Json::arrayValue; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + s = "hello"; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); - while (!(*done)) + s = 42; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); +} + + +TEST(JobsSerialization, JobOperationValues) +{ + Json::Value s; + + { + JobOperationValues values; + values.Append(new NullOperationValue); + values.Append(new StringOperationValue("hello")); + values.Append(new StringOperationValue("world")); + + s = 42; + values.Serialize(s); + } + { - ListOfStrings l; - s->GetListOfJobs(l); - for (ListOfStrings::iterator it = l.begin(); it != l.end(); ++it) - { - printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it)); - } - SystemToolbox::USleep(3000); + GenericJobUnserializer unserializer; + std::auto_ptr<JobOperationValues> values(JobOperationValues::Unserialize(unserializer, s)); + ASSERT_EQ(3u, values->GetSize()); + ASSERT_EQ(JobOperationValue::Type_Null, values->GetValue(0).GetType()); + ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(1).GetType()); + ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(2).GetType()); + + ASSERT_EQ("hello", dynamic_cast<const StringOperationValue&>(values->GetValue(1)).GetContent()); + ASSERT_EQ("world", dynamic_cast<const StringOperationValue&>(values->GetValue(2)).GetContent()); + } +} + + +TEST(JobsSerialization, GenericValues) +{ + GenericJobUnserializer unserializer; + Json::Value s; + + { + NullOperationValue null; + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null)); + null.Serialize(s); + } + + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::auto_ptr<JobOperationValue> value; + value.reset(unserializer.UnserializeValue(s)); + + ASSERT_EQ(JobOperationValue::Type_Null, value->GetType()); + + { + StringOperationValue str("Hello"); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str)); + str.Serialize(s); + } + + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + value.reset(unserializer.UnserializeValue(s)); + + ASSERT_EQ(JobOperationValue::Type_String, value->GetType()); + ASSERT_EQ("Hello", dynamic_cast<StringOperationValue&>(*value).GetContent()); +} + + +TEST(JobsSerialization, GenericOperations) +{ + DummyUnserializer unserializer; + Json::Value s; + + { + LogJobOperation operation; + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + + { + std::auto_ptr<IJobOperation> operation; + operation.reset(unserializer.UnserializeOperation(s)); + + // Make sure that we have indeed unserialized a log operation + ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation), std::bad_cast); + dynamic_cast<LogJobOperation&>(*operation); } } -TEST(MultiThreading, ServerScheduler) +TEST(JobsSerialization, GenericJobs) +{ + Json::Value s; + + // This tests SetOfInstancesJob + + { + DummyInstancesJob job; + job.SetDescription("description"); + job.AddInstance("hello"); + job.AddInstance("nope"); + job.AddInstance("world"); + job.SetPermissive(true); + ASSERT_THROW(job.ExecuteStep(), OrthancException); // Not started yet + job.Start(); + job.ExecuteStep(); + job.ExecuteStep(); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + } + + ASSERT_TRUE(job.Serialize(s)); + } + + { + DummyUnserializer unserializer; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::auto_ptr<IJob> job; + job.reset(unserializer.UnserializeJob(s)); + + const DummyInstancesJob& tmp = dynamic_cast<const DummyInstancesJob&>(*job); + ASSERT_FALSE(tmp.IsStarted()); + ASSERT_TRUE(tmp.IsPermissive()); + ASSERT_EQ("description", tmp.GetDescription()); + ASSERT_EQ(3u, tmp.GetInstancesCount()); + ASSERT_EQ(2u, tmp.GetPosition()); + ASSERT_EQ(1u, tmp.GetFailedInstances().size()); + ASSERT_EQ("hello", tmp.GetInstance(0)); + ASSERT_EQ("nope", tmp.GetInstance(1)); + ASSERT_EQ("world", tmp.GetInstance(2)); + ASSERT_TRUE(tmp.IsFailedInstance("nope")); + } + + // SequenceOfOperationsJob + + { + SequenceOfOperationsJob job; + job.SetDescription("hello"); + + { + SequenceOfOperationsJob::Lock lock(job); + size_t a = lock.AddOperation(new LogJobOperation); + size_t b = lock.AddOperation(new LogJobOperation); + lock.Connect(a, b); + lock.AddInput(a, StringOperationValue("hello")); + lock.AddInput(a, StringOperationValue("world")); + lock.SetDicomAssociationTimeout(200); + lock.SetTrailingOperationTimeout(300); + } + + ASSERT_EQ(JobStepCode_Continue, job.ExecuteStep().GetCode()); + + { + GenericJobUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + } + + ASSERT_TRUE(job.Serialize(s)); + } + + { + GenericJobUnserializer unserializer; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::auto_ptr<IJob> job; + job.reset(unserializer.UnserializeJob(s)); + + std::string tmp; + dynamic_cast<SequenceOfOperationsJob&>(*job).GetDescription(tmp); + ASSERT_EQ("hello", tmp); + } +} + + +static bool IsSameTagValue(ParsedDicomFile& dicom1, + ParsedDicomFile& dicom2, + DicomTag tag) { - ServerScheduler scheduler(10); + std::string a, b; + return (dicom1.GetTagValue(a, tag) && + dicom2.GetTagValue(b, tag) && + (a == b)); +} + + + +TEST(JobsSerialization, DicomModification) +{ + Json::Value s; + + ParsedDicomFile source(true); + source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false); + source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false); + source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false); + + std::auto_ptr<ParsedDicomFile> modified(source.Clone(true)); + + { + DicomModification modification; + modification.SetLevel(ResourceType_Series); + modification.Clear(DICOM_TAG_STUDY_DESCRIPTION); + modification.Remove(DICOM_TAG_SERIES_DESCRIPTION); + modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true); + + modification.Apply(*modified); + + s = 42; + modification.Serialize(s); + } + + { + DicomModification modification(s); + ASSERT_EQ(ResourceType_Series, modification.GetLevel()); + + std::auto_ptr<ParsedDicomFile> second(source.Clone(true)); + modification.Apply(*second); - ServerJob job; - ServerCommandInstance& f2 = job.AddCommand(new Tutu(2)); - ServerCommandInstance& f3 = job.AddCommand(new Tutu(3)); - ServerCommandInstance& f4 = job.AddCommand(new Tutu(4)); - ServerCommandInstance& f5 = job.AddCommand(new Tutu(5)); - f2.AddInput(boost::lexical_cast<std::string>(42)); - //f3.AddInput(boost::lexical_cast<std::string>(42)); - //f4.AddInput(boost::lexical_cast<std::string>(42)); - f2.ConnectOutput(f3); - f3.ConnectOutput(f4); - f4.ConnectOutput(f5); + std::string s; + ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_STUDY_DESCRIPTION)); + ASSERT_TRUE(s.empty()); + ASSERT_FALSE(second->GetTagValue(s, DICOM_TAG_SERIES_DESCRIPTION)); + ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ("Test 4", s); + + ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID)); + + ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID)); + } +} + + +TEST(JobsSerialization, DicomInstanceOrigin) +{ + Json::Value s; + + { + DicomInstanceOrigin origin; + + s = 42; + origin.Serialize(s); + } + + { + DicomInstanceOrigin origin(s); + ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin()); + ASSERT_THROW(origin.GetRemoteIp(), OrthancException); + ASSERT_EQ("", std::string(origin.GetRemoteAetC())); + ASSERT_THROW(origin.GetCalledAet(), OrthancException); + ASSERT_THROW(origin.GetHttpUsername(), OrthancException); + } + + { + DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called")); + + s = 42; + origin.Serialize(s); + } + + { + DicomInstanceOrigin origin(s); + ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin()); + ASSERT_EQ("host", origin.GetRemoteIp()); + ASSERT_EQ("aet", std::string(origin.GetRemoteAetC())); + ASSERT_EQ("called", origin.GetCalledAet()); + ASSERT_THROW(origin.GetHttpUsername(), OrthancException); + } + + { + DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username")); + + s = 42; + origin.Serialize(s); + } + + { + DicomInstanceOrigin origin(s); + ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin()); + ASSERT_EQ("host", origin.GetRemoteIp()); + ASSERT_EQ("", std::string(origin.GetRemoteAetC())); + ASSERT_THROW(origin.GetCalledAet(), OrthancException); + ASSERT_EQ("username", origin.GetHttpUsername()); + } - f3.SetConnectedToSink(true); - f5.SetConnectedToSink(true); + { + DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua()); + + s = 42; + origin.Serialize(s); + } + + { + DicomInstanceOrigin origin(s); + ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin()); + } + + { + DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins()); + + s = 42; + origin.Serialize(s); + } + + { + DicomInstanceOrigin origin(s); + ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin()); + } +} + - job.SetDescription("tutu"); +namespace +{ + class OrthancJobsSerialization : public testing::Test + { + private: + MemoryStorageArea storage_; + DatabaseWrapper db_; // The SQLite DB is in memory + std::auto_ptr<ServerContext> context_; + TimeoutDicomConnectionManager manager_; + + public: + OrthancJobsSerialization() + { + db_.Open(); + context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, + false /* don't reload jobs */)); + } - bool done = false; - boost::thread t(Tata, &scheduler, &job, &done); + virtual ~OrthancJobsSerialization() + { + context_->Stop(); + context_.reset(NULL); + db_.Close(); + } + + ServerContext& GetContext() + { + return *context_; + } + + bool CreateInstance(std::string& id) + { + // Create a sample DICOM file + ParsedDicomFile dicom(true); + dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"), + false, DicomReplaceMode_InsertIfAbsent); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(dicom); + + return (context_->Store(id, toStore) == StoreStatus_Success); + } + }; +} - //scheduler.Submit(job); +TEST_F(OrthancJobsSerialization, Values) +{ + std::string id; + ASSERT_TRUE(CreateInstance(id)); + + Json::Value s; + OrthancJobUnserializer unserializer(GetContext()); + + { + DicomInstanceOperationValue instance(GetContext(), id); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance)); + instance.Serialize(s); + } + + std::auto_ptr<JobOperationValue> value; + value.reset(unserializer.UnserializeValue(s)); + ASSERT_EQ(JobOperationValue::Type_DicomInstance, value->GetType()); + ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId()); + + { + std::string content; + dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content); - IServerCommand::ListOfStrings l; - scheduler.SubmitAndWait(l, job); + ParsedDicomFile dicom(content); + ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ("JODOGNE", content); + } +} + + +TEST_F(OrthancJobsSerialization, Operations) +{ + std::string id; + ASSERT_TRUE(CreateInstance(id)); + + Json::Value s; + OrthancJobUnserializer unserializer(GetContext()); + + // DeleteResourceOperation + + { + DeleteResourceOperation operation(GetContext()); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + + std::auto_ptr<IJobOperation> operation; + + { + operation.reset(unserializer.UnserializeOperation(s)); + + ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation), std::bad_cast); + dynamic_cast<DeleteResourceOperation&>(*operation); + } + + // StorePeerOperation - ASSERT_EQ(2u, l.size()); - ASSERT_EQ(42 * 2 * 3, boost::lexical_cast<int>(l.front())); - ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast<int>(l.back())); + { + WebServiceParameters peer; + peer.SetUrl("http://localhost/"); + peer.SetUsername("username"); + peer.SetPassword("password"); + peer.SetPkcs11Enabled(true); + + StorePeerOperation operation(peer); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + + { + operation.reset(unserializer.UnserializeOperation(s)); + + const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation); + ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl()); + ASSERT_EQ("username", tmp.GetPeer().GetUsername()); + ASSERT_EQ("password", tmp.GetPeer().GetPassword()); + ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled()); + } + + // StoreScuOperation + + { + RemoteModalityParameters modality; + modality.SetApplicationEntityTitle("REMOTE"); + modality.SetHost("192.168.1.1"); + modality.SetPort(1000); + modality.SetManufacturer(ModalityManufacturer_StoreScp); + + StoreScuOperation operation("TEST", modality); - for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++) + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + + { + operation.reset(unserializer.UnserializeOperation(s)); + + const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation); + ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); + ASSERT_EQ(1000, tmp.GetRemoteModality().GetPort()); + ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("TEST", tmp.GetLocalAet()); + } + + // SystemCallOperation + { - printf("** %s\n", i->c_str()); + SystemCallOperation operation(std::string("echo")); + operation.AddPreArgument("a"); + operation.AddPreArgument("b"); + operation.AddPostArgument("c"); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + + { + operation.reset(unserializer.UnserializeOperation(s)); + + const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation); + ASSERT_EQ("echo", tmp.GetCommand()); + ASSERT_EQ(2u, tmp.GetPreArgumentsCount()); + ASSERT_EQ(1u, tmp.GetPostArgumentsCount()); + ASSERT_EQ("a", tmp.GetPreArgument(0)); + ASSERT_EQ("b", tmp.GetPreArgument(1)); + ASSERT_EQ("c", tmp.GetPostArgument(0)); } - //SystemToolbox::ServerBarrier(); - //SystemToolbox::USleep(3000000); + // ModifyInstanceOperation - scheduler.Stop(); + { + std::auto_ptr<DicomModification> modification(new DicomModification); + modification->SetupAnonymization(DicomVersion_2008); + + ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release()); - done = true; - if (t.joinable()) + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } + { - t.join(); + operation.reset(unserializer.UnserializeOperation(s)); + + const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation); + ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin()); + ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION)); } } + + +TEST_F(OrthancJobsSerialization, Jobs) +{ + // ArchiveJob + + Json::Value s; + + { + boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile); + ArchiveJob job(tmp, GetContext(), false, false); + ASSERT_FALSE(job.Serialize(s)); // Cannot serialize this + } + + // DicomModalityStoreJob + + OrthancJobUnserializer unserializer(GetContext()); + + { + RemoteModalityParameters modality; + modality.SetApplicationEntityTitle("REMOTE"); + modality.SetHost("192.168.1.1"); + modality.SetPort(1000); + modality.SetManufacturer(ModalityManufacturer_StoreScp); + + DicomModalityStoreJob job(GetContext()); + job.SetLocalAet("LOCAL"); + job.SetRemoteModality(modality); + job.SetMoveOriginator("MOVESCU", 42); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::auto_ptr<IJob> job; + job.reset(unserializer.UnserializeJob(s)); + + DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job); + ASSERT_EQ("LOCAL", tmp.GetLocalAet()); + ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); + ASSERT_EQ(1000, tmp.GetRemoteModality().GetPort()); + ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); + ASSERT_TRUE(tmp.HasMoveOriginator()); + ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet()); + ASSERT_EQ(42, tmp.GetMoveOriginatorId()); + } + + // OrthancPeerStoreJob + + { + WebServiceParameters peer; + peer.SetUrl("http://localhost/"); + peer.SetUsername("username"); + peer.SetPassword("password"); + peer.SetPkcs11Enabled(true); + + OrthancPeerStoreJob job(GetContext()); + job.SetPeer(peer); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::auto_ptr<IJob> job; + job.reset(unserializer.UnserializeJob(s)); + + OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job); + ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl()); + ASSERT_EQ("username", tmp.GetPeer().GetUsername()); + ASSERT_EQ("password", tmp.GetPeer().GetPassword()); + ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled()); + } + + // ResourceModificationJob + + { + std::auto_ptr<DicomModification> modification(new DicomModification); + modification->SetupAnonymization(DicomVersion_2008); + + ResourceModificationJob job(GetContext()); + job.SetModification(modification.release(), true); + job.SetOrigin(DicomInstanceOrigin::FromLua()); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::auto_ptr<IJob> job; + job.reset(unserializer.UnserializeJob(s)); + + ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job); + ASSERT_TRUE(tmp.IsAnonymization()); + ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin()); + ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION)); + } +} + + +TEST(JobsSerialization, Registry) +{ + Json::Value s; + std::string i1, i2; + + { + JobsRegistry registry; + registry.Submit(i1, new DummyJob(), 10); + registry.Submit(i2, new SequenceOfOperationsJob(), 30); + registry.Serialize(s); + } + + { + DummyUnserializer unserializer; + JobsRegistry registry(unserializer, s); + + Json::Value t; + registry.Serialize(t); + ASSERT_TRUE(CheckSameJson(s, t)); + } +}
--- a/UnitTestsSources/SQLiteChromiumTests.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/UnitTestsSources/SQLiteChromiumTests.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -51,35 +51,38 @@ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc ********************************************************************/ -class SQLConnectionTest : public testing::Test +namespace { -public: - SQLConnectionTest() - { - } - - virtual ~SQLConnectionTest() - { - } - - virtual void SetUp() + class SQLConnectionTest : public testing::Test { - db_.OpenInMemory(); - } + public: + SQLConnectionTest() + { + } - virtual void TearDown() - { - db_.Close(); - } + virtual ~SQLConnectionTest() + { + } + + virtual void SetUp() + { + db_.OpenInMemory(); + } - Connection& db() - { - return db_; - } + virtual void TearDown() + { + db_.Close(); + } -private: - Connection db_; -}; + Connection& db() + { + return db_; + } + + private: + Connection db_; + }; +} @@ -266,23 +269,26 @@ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc ********************************************************************/ -class SQLTransactionTest : public SQLConnectionTest +namespace { -public: - virtual void SetUp() + class SQLTransactionTest : public SQLConnectionTest { - SQLConnectionTest::SetUp(); - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - } + public: + virtual void SetUp() + { + SQLConnectionTest::SetUp(); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + } - // Returns the number of rows in table "foo". - int CountFoo() - { - Statement count(db(), "SELECT count(*) FROM foo"); - count.Step(); - return count.ColumnInt(0); - } -}; + // Returns the number of rows in table "foo". + int CountFoo() + { + Statement count(db(), "SELECT count(*) FROM foo"); + count.Step(); + return count.ColumnInt(0); + } + }; +} TEST_F(SQLTransactionTest, Commit) {
--- a/UnitTestsSources/ServerIndexTests.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -675,7 +675,8 @@ FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage); + ServerContext context(db, storage, true /* running unit tests */, + false /* don't reload jobs */); ServerIndex& index = context.GetIndex(); ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); @@ -773,7 +774,8 @@ FilesystemStorage storage(path); DatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage); + ServerContext context(db, storage, true /* running unit tests */, + false /* don't reload jobs */); ServerIndex& index = context.GetIndex(); index.SetMaximumStorageSize(10);
--- a/UnitTestsSources/UnitTestsMain.cpp Thu Jun 07 17:15:55 2018 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Mon Jun 11 16:30:13 2018 +0200 @@ -677,6 +677,14 @@ } ASSERT_THROW(StringToValueRepresentation("nope", true), OrthancException); + + ASSERT_EQ(JobState_Pending, StringToJobState(EnumerationToString(JobState_Pending))); + ASSERT_EQ(JobState_Running, StringToJobState(EnumerationToString(JobState_Running))); + ASSERT_EQ(JobState_Success, StringToJobState(EnumerationToString(JobState_Success))); + ASSERT_EQ(JobState_Failure, StringToJobState(EnumerationToString(JobState_Failure))); + ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused))); + ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry))); + ASSERT_THROW(StringToJobState("nope"), OrthancException); }