Mercurial > hg > orthanc
changeset 2573:3372c5255333 jobs
StoreScuJob, Orthanc Explorer for jobs
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 09 May 2018 17:56:14 +0200 |
parents | 2e879c796ec7 |
children | 84cbc5abf3cc |
files | Core/JobsEngine/IJob.h Core/JobsEngine/JobInfo.cpp Core/JobsEngine/JobInfo.h Core/JobsEngine/JobStatus.cpp Core/JobsEngine/JobStatus.h Core/JobsEngine/JobsEngine.cpp Core/JobsEngine/JobsEngine.h Core/JobsEngine/JobsRegistry.cpp Core/JobsEngine/JobsRegistry.h OrthancExplorer/explorer.html OrthancExplorer/explorer.js OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ServerIndex.cpp UnitTestsSources/MultiThreadingTests.cpp |
diffstat | 15 files changed, 719 insertions(+), 380 deletions(-) [+] |
line wrap: on
line diff
--- a/Core/JobsEngine/IJob.h Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/IJob.h Wed May 09 17:56:14 2018 +0200 @@ -56,6 +56,10 @@ virtual float GetProgress() = 0; - virtual void GetDescription(Json::Value& value) = 0; + virtual void GetJobType(std::string& target) = 0; + + virtual void GetPublicContent(Json::Value& value) = 0; + + virtual void GetInternalContent(Json::Value& value) = 0; }; }
--- a/Core/JobsEngine/JobInfo.cpp Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobInfo.cpp Wed May 09 17:56:14 2018 +0200 @@ -62,7 +62,8 @@ if (status_.GetProgress() > 0.01f && ms > 0.01f) { - float remaining = boost::math::llround(1.0f - status_.GetProgress()) * ms; + 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; } @@ -115,7 +116,7 @@ } - void JobInfo::Format(Json::Value& target) const + void JobInfo::Serialize(Json::Value& target) const { target = Json::objectValue; target["ID"] = id_; @@ -125,9 +126,12 @@ target["State"] = EnumerationToString(state_); target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_); target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_); - target["Runtime"] = static_cast<uint32_t>(runtime_.total_milliseconds()); + target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0; target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f); - target["Description"] = status_.GetDescription(); + + target["Type"] = status_.GetJobType(); + target["PublicContent"] = status_.GetPublicContent(); + target["InternalContent"] = status_.GetInternalContent(); if (HasEstimatedTimeOfArrival()) {
--- a/Core/JobsEngine/JobInfo.h Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobInfo.h Wed May 09 17:56:14 2018 +0200 @@ -115,6 +115,6 @@ return status_; } - void Format(Json::Value& target) const; + void Serialize(Json::Value& target) const; }; }
--- a/Core/JobsEngine/JobStatus.cpp Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobStatus.cpp Wed May 09 17:56:14 2018 +0200 @@ -39,7 +39,9 @@ JobStatus::JobStatus() : errorCode_(ErrorCode_InternalError), progress_(0), - description_(Json::objectValue) + jobType_("Invalid"), + publicContent_(Json::objectValue), + internalContent_(Json::objectValue) { } @@ -47,7 +49,9 @@ JobStatus::JobStatus(ErrorCode code, IJob& job) : errorCode_(code), - progress_(job.GetProgress()) + progress_(job.GetProgress()), + publicContent_(Json::objectValue), + internalContent_(Json::objectValue) { if (progress_ < 0) { @@ -59,6 +63,8 @@ progress_ = 1; } - job.GetDescription(description_); + job.GetJobType(jobType_); + job.GetPublicContent(publicContent_); + job.GetInternalContent(internalContent_); } }
--- a/Core/JobsEngine/JobStatus.h Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobStatus.h Wed May 09 17:56:14 2018 +0200 @@ -42,7 +42,9 @@ private: ErrorCode errorCode_; float progress_; - Json::Value description_; + std::string jobType_; + Json::Value publicContent_; + Json::Value internalContent_; public: JobStatus(); @@ -60,9 +62,19 @@ return progress_; } - const Json::Value& GetDescription() const + const std::string& GetJobType() const + { + return jobType_; + } + + const Json::Value& GetPublicContent() const { - return description_; + return publicContent_; + } + + const Json::Value& GetInternalContent() const + { + return internalContent_; } }; }
--- a/Core/JobsEngine/JobsEngine.cpp Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobsEngine.cpp Wed May 09 17:56:14 2018 +0200 @@ -41,14 +41,18 @@ 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()); - LOG(INFO) << "Executing job with priority " << running.GetPriority() - << " in worker thread " << workerIndex << ": " << running.GetId(); - if (running.IsPauseScheduled()) { running.GetJob().ReleaseResources(); @@ -94,10 +98,12 @@ switch (result->GetCode()) { case JobStepCode_Success: + running.GetJob().ReleaseResources(); running.MarkSuccess(); return false; case JobStepCode_Failure: + running.GetJob().ReleaseResources(); running.MarkFailure(); return false; @@ -119,19 +125,9 @@ { assert(engine != NULL); - for (;;) + while (engine->IsRunning()) { boost::this_thread::sleep(boost::posix_time::milliseconds(200)); - - { - boost::mutex::scoped_lock lock(engine->stateMutex_); - - if (engine->state_ != State_Running) - { - return; - } - } - engine->GetRegistry().ScheduleRetries(); } } @@ -144,22 +140,16 @@ LOG(INFO) << "Worker thread " << workerIndex << " has started"; - for (;;) + while (engine->IsRunning()) { - { - boost::mutex::scoped_lock lock(engine->stateMutex_); - - if (engine->state_ != State_Running) - { - return; - } - } - JobsRegistry::RunningJob running(engine->GetRegistry(), 100); if (running.IsValid()) { - for (;;) + LOG(INFO) << "Executing job with priority " << running.GetPriority() + << " in worker thread " << workerIndex << ": " << running.GetId(); + + while (engine->IsRunning()) { if (!engine->ExecuteStep(running, workerIndex)) {
--- a/Core/JobsEngine/JobsEngine.h Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobsEngine.h Wed May 09 17:56:14 2018 +0200 @@ -56,6 +56,8 @@ boost::thread retryHandler_; std::vector<boost::thread> workers_; + bool IsRunning(); + bool ExecuteStep(JobsRegistry::RunningJob& running, size_t workerIndex);
--- a/Core/JobsEngine/JobsRegistry.cpp Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobsRegistry.cpp Wed May 09 17:56:14 2018 +0200 @@ -520,7 +520,7 @@ } - void JobsRegistry::SetPriority(const std::string& id, + bool JobsRegistry::SetPriority(const std::string& id, int priority) { LOG(INFO) << "Changing priority to " << priority << " for job: " << id; @@ -533,6 +533,7 @@ if (found == jobsIndex_.end()) { LOG(WARNING) << "Unknown job: " << id; + return false; } else { @@ -553,13 +554,14 @@ copy.pop(); } } + + CheckInvariants(); + return true; } - - CheckInvariants(); } - void JobsRegistry::Pause(const std::string& id) + bool JobsRegistry::Pause(const std::string& id) { LOG(INFO) << "Pausing job: " << id; @@ -571,6 +573,7 @@ if (found == jobsIndex_.end()) { LOG(WARNING) << "Unknown job: " << id; + return false; } else { @@ -623,13 +626,14 @@ default: throw OrthancException(ErrorCode_InternalError); } + + CheckInvariants(); + return true; } - - CheckInvariants(); } - void JobsRegistry::Resume(const std::string& id) + bool JobsRegistry::Resume(const std::string& id) { LOG(INFO) << "Resuming job: " << id; @@ -641,23 +645,25 @@ 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; } - - CheckInvariants(); } - void JobsRegistry::Resubmit(const std::string& id) + bool JobsRegistry::Resubmit(const std::string& id) { LOG(INFO) << "Resubmitting failed job: " << id; @@ -669,10 +675,12 @@ 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 { @@ -693,9 +701,10 @@ found->second->SetState(JobState_Pending); pendingJobs_.push(found->second); pendingJobAvailable_.notify_one(); + + CheckInvariants(); + return true; } - - CheckInvariants(); }
--- a/Core/JobsEngine/JobsRegistry.h Mon May 07 21:42:04 2018 +0200 +++ b/Core/JobsEngine/JobsRegistry.h Wed May 09 17:56:14 2018 +0200 @@ -130,14 +130,14 @@ bool SubmitAndWait(IJob* job, // Takes ownership int priority); - void SetPriority(const std::string& id, + bool SetPriority(const std::string& id, int priority); - void Pause(const std::string& id); + bool Pause(const std::string& id); - void Resume(const std::string& id); + bool Resume(const std::string& id); - void Resubmit(const std::string& id); + bool Resubmit(const std::string& id); void ScheduleRetries();
--- a/OrthancExplorer/explorer.html Mon May 07 21:42:04 2018 +0200 +++ b/OrthancExplorer/explorer.html Wed May 09 17:56:14 2018 +0200 @@ -37,7 +37,10 @@ <div data-role="page" id="find-patients" > <div data-role="header" > <h1><span class="orthanc-name"></span>Find a patient</h1> - <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> + <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a> + <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</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> @@ -418,6 +421,43 @@ </div> </div> + + <div data-role="page" id="jobs" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>Jobs</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + </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="#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-delete" data-theme="b">Delete job</button> + <button id="job-retry" data-theme="b">Retry 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 Mon May 07 21:42:04 2018 +0200 +++ b/OrthancExplorer/explorer.js Wed May 09 17:56:14 2018 +0200 @@ -1104,3 +1104,140 @@ } }); }); + + + +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(); + + 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.PublicContent.LocalAet); + AddJobField(item, 'Remote AET: ', job.PublicContent.RemoteAet); + AddJobDateField(item, 'Creation time: ', job.CreationTime); + AddJobDateField(item, 'Completion time: ', job.CompletionTime); + AddJobDateField(item, 'ETA: ', job.EstimatedTimeOfArrival); + target.append(li); + }); + + 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 != 'PublicContent' && + 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 i in job.PublicContent) { + AddJobField(block, i + ': ', JSON.stringify(job.PublicContent[i])); + } + + target.append(block); + + target.listview('refresh'); + + $('#job-delete').closest('.ui-btn').show(); + $('#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-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(); + } + } + }); + } +});
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Mon May 07 21:42:04 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 09 17:56:14 2018 +0200 @@ -44,6 +44,352 @@ #include "../QueryRetrieveHandler.h" #include "../ServerToolbox.h" + + +namespace Orthanc +{ + class InstancesIteratorJob : public IJob + { + private: + bool started_; + std::vector<std::string> instances_; + size_t position_; + + public: + InstancesIteratorJob() : + started_(false), + position_(0) + { + } + + void Reserve(size_t size) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instances_.reserve(size); + } + } + + size_t GetInstancesCount() const + { + return instances_.size(); + } + + void AddInstance(const std::string& instance) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instances_.push_back(instance); + } + } + + virtual void Start() + { + started_ = true; + } + + virtual float GetProgress() + { + if (instances_.size() == 0) + { + return 0; + } + else + { + return (static_cast<float>(position_) / + static_cast<float>(instances_.size())); + } + } + + bool IsStarted() const + { + return started_; + } + + bool IsDone() const + { + return (position_ >= instances_.size()); + } + + void Next() + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + position_ += 1; + } + } + + const std::string& GetCurrentInstance() const + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return instances_[position_]; + } + } + }; + + + class StoreScuJob : public InstancesIteratorJob + { + private: + ServerContext& context_; + std::string localAet_; + RemoteModalityParameters remote_; + bool permissive_; + std::string moveOriginatorAet_; + uint16_t moveOriginatorId_; + std::auto_ptr<DicomUserConnection> connection_; + std::set<std::string> failedInstances_; + + void Open() + { + if (connection_.get() == NULL) + { + connection_.reset(new DicomUserConnection); + connection_->SetLocalApplicationEntityTitle(localAet_); + connection_->SetRemoteModality(remote_); + connection_->Open(); + } + } + + public: + StoreScuJob(ServerContext& context) : + context_(context), + localAet_("ORTHANC"), + permissive_(false), + moveOriginatorId_(0) // By default, not a C-MOVE + { + } + + void AddResource(const std::string& publicId) + { + typedef std::list<std::string> Instances; + + Instances instances; + context_.GetIndex().GetChildInstances(instances, publicId); + + Reserve(GetInstancesCount() + instances.size()); + + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + AddInstance(*it); + } + } + + const std::string& GetLocalAet() const + { + return localAet_; + } + + void SetLocalAet(const std::string& aet) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + localAet_ = aet; + } + } + + const RemoteModalityParameters& GetRemoteModality() const + { + return remote_; + } + + void SetRemoteModality(const RemoteModalityParameters& remote) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + remote_ = remote; + } + } + + bool IsPermissive() const + { + return permissive_; + } + + void SetPermissive(bool permissive) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + permissive_ = permissive; + } + } + + bool HasMoveOriginator() const + { + return moveOriginatorId_ != 0; + } + + const std::string& GetMoveOriginatorAet() const + { + if (HasMoveOriginator()) + { + return moveOriginatorAet_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + uint16_t GetMoveOriginatorId() const + { + if (HasMoveOriginator()) + { + return moveOriginatorId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void 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; + } + } + + virtual JobStepResult* ExecuteStep() + { + if (IsDone()) + { + return new JobStepResult(JobStepCode_Success); + } + + Open(); + + bool ok = false; + + try + { + std::string dicom; + context_.ReadDicom(dicom, GetCurrentInstance()); + + if (HasMoveOriginator()) + { + connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_); + } + else + { + connection_->Store(dicom); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(300)); + + ok = true; + } + catch (OrthancException& e) + { + } + + if (!ok) + { + if (permissive_) + { + failedInstances_.insert(GetCurrentInstance()); + } + else + { + return new JobStepResult(JobStepCode_Failure); + } + } + + Next(); + + if (IsDone()) + { + return new JobStepResult(JobStepCode_Success); + } + else + { + return new JobStepResult(JobStepCode_Continue); + } + } + + virtual void ReleaseResources() // For pausing jobs + { + connection_.reset(NULL); + } + + virtual void GetJobType(std::string& target) + { + target = "C-Store"; + } + + virtual void GetPublicContent(Json::Value& value) + { + value["LocalAet"] = localAet_; + value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + + if (HasMoveOriginator()) + { + value["MoveOriginatorAET"] = GetMoveOriginatorAet(); + value["MoveOriginatorID"] = GetMoveOriginatorId(); + } + + Json::Value v = Json::arrayValue; + for (std::set<std::string>::const_iterator it = failedInstances_.begin(); + it != failedInstances_.end(); ++it) + { + v.append(*it); + } + + value["FailedInstances"] = v; + } + + virtual void GetInternalContent(Json::Value& value) + { + // TODO + } + }; +} + + + + namespace Orthanc { /*************************************************************************** @@ -689,6 +1035,7 @@ 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 */); + int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0); if (moveOriginatorID < 0 || moveOriginatorID >= 65536) @@ -698,6 +1045,44 @@ RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote); +#if 1 + std::auto_ptr<StoreScuJob> job(new StoreScuJob(context)); + job->SetLocalAet(localAet); + job->SetRemoteModality(p); + job->SetPermissive(permissive); + + if (moveOriginatorID != 0) + { + job->SetMoveOriginator(moveOriginatorAET, static_cast<uint16_t>(moveOriginatorID)); + } + + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + job->AddInstance(*it); + } + + 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); + } + +#else ServerJob job; for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -729,6 +1114,7 @@ { call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } +#endif }
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Mon May 07 21:42:04 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Wed May 09 17:56:14 2018 +0200 @@ -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,65 @@ } - static void GetDefaultEncoding(RestApiGetCall& call) - { - Encoding encoding = GetDefaultDicomEncoding(); - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); - } - static void SetDefaultEncoding(RestApiPutCall& call) + // Jobs information ------------------------------------------------------ + + static void ListJobs(RestApiGetCall& call) { - Encoding encoding = StringToEncoding(call.GetBodyData()); + bool expand = call.HasArgument("expand"); + + Json::Value v = Json::arrayValue; + + std::set<std::string> jobs; + OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs); - Configuration::SetDefaultEncoding(encoding); - - call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain"); + 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.Serialize(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.Serialize(json); + call.GetOutput().AnswerJson(json); + } + } + + static void PauseJob(RestApiPostCall& call) + { + std::string id = call.GetUriComponent("id", ""); + + if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id)) + { + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + } + + void OrthancRestApi::RegisterSystem() { Register("/", ServeRoot); @@ -284,5 +344,9 @@ Register("/plugins", ListPlugins); Register("/plugins/{id}", GetPlugin); Register("/plugins/explorer.js", GetOrthancExplorerPlugins); + + Register("/jobs", ListJobs); + Register("/jobs/{id}", GetJobInfo); + Register("/jobs/{id}/pause", PauseJob); } }
--- a/OrthancServer/ServerIndex.cpp Mon May 07 21:42:04 2018 +0200 +++ b/OrthancServer/ServerIndex.cpp Wed May 09 17:56:14 2018 +0200 @@ -1212,7 +1212,7 @@ ResourceType type; if (!db_.LookupResource(id, type, publicId)) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InexistentItem); } std::string patientId;
--- a/UnitTestsSources/MultiThreadingTests.cpp Mon May 07 21:42:04 2018 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed May 09 17:56:14 2018 +0200 @@ -603,321 +603,6 @@ -#include "../OrthancServer/ServerContext.h" - -namespace Orthanc -{ - class InstancesIteratorJob : public IJob - { - private: - bool started_; - std::vector<std::string> instances_; - size_t position_; - - public: - InstancesIteratorJob() : - started_(false), - position_(0) - { - } - - void Reserve(size_t size) - { - if (started_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - instances_.reserve(size); - } - } - - void AddInstance(const std::string& instance) - { - if (started_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - instances_.push_back(instance); - } - } - - virtual void Start() - { - started_ = true; - } - - virtual float GetProgress() - { - if (instances_.size() == 0) - { - return 0; - } - else - { - return (static_cast<float>(position_) / - static_cast<float>(instances_.size())); - } - } - - bool IsStarted() const - { - return started_; - } - - bool IsDone() const - { - if (instances_.size() == 0) - { - return true; - } - else - { - return (position_ == instances_.size() - 1); - } - } - - void Next() - { - if (IsDone()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - position_ += 1; - } - } - - const std::string& GetCurrentInstance() const - { - if (IsDone()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return instances_[position_]; - } - } - }; - - - class StoreScuJob : public InstancesIteratorJob - { - private: - ServerContext& context_; - std::string localAet_; - RemoteModalityParameters remote_; - bool permissive_; - std::string moveOriginatorAet_; - uint16_t moveOriginatorId_; - std::auto_ptr<DicomUserConnection> connection_; - std::set<std::string> failedInstances_; - - void Open() - { - if (connection_.get() == NULL) - { - connection_.reset(new DicomUserConnection); - connection_->SetLocalApplicationEntityTitle(localAet_); - connection_->SetRemoteModality(remote_); - connection_->Open(); - } - } - - public: - StoreScuJob(ServerContext& context) : - context_(context), - localAet_("ORTHANC"), - permissive_(false), - moveOriginatorId_(0) // By default, not a C-MOVE - { - } - - const std::string& GetLocalAet() const - { - return localAet_; - } - - void SetLocalAet(const std::string& aet) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - localAet_ = aet; - } - } - - const RemoteModalityParameters& GetRemoteModality() const - { - return remote_; - } - - void SetRemoteModality(const RemoteModalityParameters& remote) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - remote_ = remote; - } - } - - bool IsPermissive() const - { - return permissive_; - } - - void SetPermissive(bool permissive) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - permissive_ = permissive; - } - } - - bool HasMoveOriginator() const - { - return moveOriginatorId_ != 0; - } - - const std::string& GetMoveOriginatorAet() const - { - if (HasMoveOriginator()) - { - return moveOriginatorAet_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - } - - uint16_t GetMoveOriginatorId() const - { - if (HasMoveOriginator()) - { - return moveOriginatorId_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - } - - void 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; - } - } - - virtual JobStepResult* ExecuteStep() - { - if (IsDone()) - { - return new JobStepResult(JobStepCode_Success); - } - - Open(); - - bool ok = false; - - try - { - std::string dicom; - context_.ReadDicom(dicom, GetCurrentInstance()); - - if (HasMoveOriginator()) - { - connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_); - } - else - { - connection_->Store(dicom); - } - - ok = true; - } - catch (OrthancException& e) - { - } - - if (!ok) - { - if (permissive_) - { - failedInstances_.insert(GetCurrentInstance()); - } - else - { - return new JobStepResult(JobStepCode_Failure); - } - } - - Next(); - - return new JobStepResult(IsDone() ? JobStepCode_Success : JobStepCode_Continue); - } - - virtual void ReleaseResources() // For pausing jobs - { - connection_.release(); - } - - virtual void GetDescription(Json::Value& value) - { - value["Type"] = "C-STORE"; - value["LocalAet"] = localAet_; - - Json::Value v; - remote_.ToJson(v); - value["Target"] = v; - - if (HasMoveOriginator()) - { - value["MoveOriginatorAET"] = GetMoveOriginatorAet(); - value["MoveOriginatorID"] = GetMoveOriginatorId(); - } - - v = Json::arrayValue; - for (std::set<std::string>::const_iterator it = failedInstances_.begin(); - it != failedInstances_.end(); ++it) - { - v.append(*it); - } - - value["FailedInstances"] = v; - } - }; -} - - TEST(JobsEngine, Basic) {