comparison OrthancServer/Sources/main.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/main.cpp@e3b3af80732d
children 05b8fd21089c
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 #include "PrecompiledHeadersServer.h"
35 #include "OrthancRestApi/OrthancRestApi.h"
36
37 #include <boost/algorithm/string/predicate.hpp>
38
39 #include "../Core/Compatibility.h"
40 #include "../Core/DicomFormat/DicomArray.h"
41 #include "../Core/DicomNetworking/DicomAssociationParameters.h"
42 #include "../Core/DicomNetworking/DicomServer.h"
43 #include "../Core/DicomParsing/FromDcmtkBridge.h"
44 #include "../Core/HttpServer/FilesystemHttpHandler.h"
45 #include "../Core/HttpServer/HttpServer.h"
46 #include "../Core/Logging.h"
47 #include "../Core/Lua/LuaFunctionCall.h"
48 #include "../Plugins/Engine/OrthancPlugins.h"
49 #include "EmbeddedResourceHttpHandler.h"
50 #include "OrthancConfiguration.h"
51 #include "OrthancFindRequestHandler.h"
52 #include "OrthancGetRequestHandler.h"
53 #include "OrthancInitialization.h"
54 #include "OrthancMoveRequestHandler.h"
55 #include "ServerContext.h"
56 #include "ServerJobs/StorageCommitmentScpJob.h"
57 #include "ServerToolbox.h"
58 #include "StorageCommitmentReports.h"
59
60 using namespace Orthanc;
61
62
63 class OrthancStoreRequestHandler : public IStoreRequestHandler
64 {
65 private:
66 ServerContext& context_;
67
68 public:
69 OrthancStoreRequestHandler(ServerContext& context) :
70 context_(context)
71 {
72 }
73
74
75 virtual void Handle(const std::string& dicomFile,
76 const DicomMap& dicomSummary,
77 const Json::Value& dicomJson,
78 const std::string& remoteIp,
79 const std::string& remoteAet,
80 const std::string& calledAet)
81 {
82 if (dicomFile.size() > 0)
83 {
84 DicomInstanceToStore toStore;
85 toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
86 (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
87 toStore.SetBuffer(dicomFile.c_str(), dicomFile.size());
88 toStore.SetSummary(dicomSummary);
89 toStore.SetJson(dicomJson);
90
91 std::string id;
92 context_.Store(id, toStore, StoreInstanceMode_Default);
93 }
94 }
95 };
96
97
98
99 class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
100 {
101 private:
102 ServerContext& context_;
103
104 public:
105 OrthancStorageCommitmentRequestHandler(ServerContext& context) :
106 context_(context)
107 {
108 }
109
110 virtual void HandleRequest(const std::string& transactionUid,
111 const std::vector<std::string>& referencedSopClassUids,
112 const std::vector<std::string>& referencedSopInstanceUids,
113 const std::string& remoteIp,
114 const std::string& remoteAet,
115 const std::string& calledAet)
116 {
117 if (referencedSopClassUids.size() != referencedSopInstanceUids.size())
118 {
119 throw OrthancException(ErrorCode_InternalError);
120 }
121
122 std::unique_ptr<StorageCommitmentScpJob> job(
123 new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet));
124
125 for (size_t i = 0; i < referencedSopClassUids.size(); i++)
126 {
127 job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]);
128 }
129
130 job->MarkAsReady();
131
132 context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */);
133 }
134
135 virtual void HandleReport(const std::string& transactionUid,
136 const std::vector<std::string>& successSopClassUids,
137 const std::vector<std::string>& successSopInstanceUids,
138 const std::vector<std::string>& failedSopClassUids,
139 const std::vector<std::string>& failedSopInstanceUids,
140 const std::vector<StorageCommitmentFailureReason>& failureReasons,
141 const std::string& remoteIp,
142 const std::string& remoteAet,
143 const std::string& calledAet)
144 {
145 if (successSopClassUids.size() != successSopInstanceUids.size() ||
146 failedSopClassUids.size() != failedSopInstanceUids.size() ||
147 failedSopClassUids.size() != failureReasons.size())
148 {
149 throw OrthancException(ErrorCode_InternalError);
150 }
151
152 std::unique_ptr<StorageCommitmentReports::Report> report(
153 new StorageCommitmentReports::Report(remoteAet));
154
155 for (size_t i = 0; i < successSopClassUids.size(); i++)
156 {
157 report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
158 }
159
160 for (size_t i = 0; i < failedSopClassUids.size(); i++)
161 {
162 report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
163 }
164
165 report->MarkAsComplete();
166
167 context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
168 }
169 };
170
171
172
173 class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
174 {
175 public:
176 virtual bool IsSameAETitle(const std::string& aet1,
177 const std::string& aet2)
178 {
179 OrthancConfiguration::ReaderLock lock;
180 return lock.GetConfiguration().IsSameAETitle(aet1, aet2);
181 }
182
183 virtual bool LookupAETitle(RemoteModalityParameters& modality,
184 const std::string& aet)
185 {
186 OrthancConfiguration::ReaderLock lock;
187 return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet);
188 }
189 };
190
191
192 class MyDicomServerFactory :
193 public IStoreRequestHandlerFactory,
194 public IFindRequestHandlerFactory,
195 public IMoveRequestHandlerFactory,
196 public IGetRequestHandlerFactory,
197 public IStorageCommitmentRequestHandlerFactory
198 {
199 private:
200 ServerContext& context_;
201
202 public:
203 MyDicomServerFactory(ServerContext& context) : context_(context)
204 {
205 }
206
207 virtual IStoreRequestHandler* ConstructStoreRequestHandler()
208 {
209 return new OrthancStoreRequestHandler(context_);
210 }
211
212 virtual IFindRequestHandler* ConstructFindRequestHandler()
213 {
214 std::unique_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
215
216 {
217 OrthancConfiguration::ReaderLock lock;
218 result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
219 result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
220 }
221
222 if (result->GetMaxResults() == 0)
223 {
224 LOG(INFO) << "No limit on the number of C-FIND results at the Patient, Study and Series levels";
225 }
226 else
227 {
228 LOG(INFO) << "Maximum " << result->GetMaxResults()
229 << " results for C-FIND queries at the Patient, Study and Series levels";
230 }
231
232 if (result->GetMaxInstances() == 0)
233 {
234 LOG(INFO) << "No limit on the number of C-FIND results at the Instance level";
235 }
236 else
237 {
238 LOG(INFO) << "Maximum " << result->GetMaxInstances()
239 << " instances will be returned for C-FIND queries at the Instance level";
240 }
241
242 return result.release();
243 }
244
245 virtual IMoveRequestHandler* ConstructMoveRequestHandler()
246 {
247 return new OrthancMoveRequestHandler(context_);
248 }
249
250 virtual IGetRequestHandler* ConstructGetRequestHandler()
251 {
252 return new OrthancGetRequestHandler(context_);
253 }
254
255 virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
256 {
257 return new OrthancStorageCommitmentRequestHandler(context_);
258 }
259
260
261 void Done()
262 {
263 }
264 };
265
266
267 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
268 {
269 private:
270 ServerContext& context_;
271 bool alwaysAllowEcho_;
272 bool alwaysAllowStore_;
273
274 public:
275 OrthancApplicationEntityFilter(ServerContext& context) :
276 context_(context)
277 {
278 OrthancConfiguration::ReaderLock lock;
279 alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
280 alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
281 }
282
283 virtual bool IsAllowedConnection(const std::string& remoteIp,
284 const std::string& remoteAet,
285 const std::string& calledAet)
286 {
287 LOG(INFO) << "Incoming connection from AET " << remoteAet
288 << " on IP " << remoteIp << ", calling AET " << calledAet;
289
290 if (alwaysAllowEcho_ ||
291 alwaysAllowStore_)
292 {
293 return true;
294 }
295 else
296 {
297 OrthancConfiguration::ReaderLock lock;
298 return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp);
299 }
300 }
301
302 virtual bool IsAllowedRequest(const std::string& remoteIp,
303 const std::string& remoteAet,
304 const std::string& calledAet,
305 DicomRequestType type)
306 {
307 LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
308 << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
309
310 if (type == DicomRequestType_Echo &&
311 alwaysAllowEcho_)
312 {
313 // Incoming C-Echo requests are always accepted, even from unknown AET
314 return true;
315 }
316 else if (type == DicomRequestType_Store &&
317 alwaysAllowStore_)
318 {
319 // Incoming C-Store requests are always accepted, even from unknown AET
320 return true;
321 }
322 else
323 {
324 OrthancConfiguration::ReaderLock lock;
325
326 RemoteModalityParameters modality;
327 if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, remoteAet))
328 {
329 return modality.IsRequestAllowed(type);
330 }
331 else
332 {
333 return false;
334 }
335 }
336 }
337
338 virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
339 const std::string& remoteAet,
340 const std::string& calledAet,
341 TransferSyntax syntax)
342 {
343 std::string configuration;
344
345 switch (syntax)
346 {
347 case TransferSyntax_Deflated:
348 configuration = "DeflatedTransferSyntaxAccepted";
349 break;
350
351 case TransferSyntax_Jpeg:
352 configuration = "JpegTransferSyntaxAccepted";
353 break;
354
355 case TransferSyntax_Jpeg2000:
356 configuration = "Jpeg2000TransferSyntaxAccepted";
357 break;
358
359 case TransferSyntax_JpegLossless:
360 configuration = "JpegLosslessTransferSyntaxAccepted";
361 break;
362
363 case TransferSyntax_Jpip:
364 configuration = "JpipTransferSyntaxAccepted";
365 break;
366
367 case TransferSyntax_Mpeg2:
368 configuration = "Mpeg2TransferSyntaxAccepted";
369 break;
370
371 case TransferSyntax_Mpeg4:
372 configuration = "Mpeg4TransferSyntaxAccepted";
373 break;
374
375 case TransferSyntax_Rle:
376 configuration = "RleTransferSyntaxAccepted";
377 break;
378
379 default:
380 throw OrthancException(ErrorCode_ParameterOutOfRange);
381 }
382
383 {
384 std::string name = "Is" + configuration;
385
386 LuaScripting::Lock lock(context_.GetLuaScripting());
387
388 if (lock.GetLua().IsExistingFunction(name.c_str()))
389 {
390 LuaFunctionCall call(lock.GetLua(), name.c_str());
391 call.PushString(remoteAet);
392 call.PushString(remoteIp);
393 call.PushString(calledAet);
394 return call.ExecutePredicate();
395 }
396 }
397
398 {
399 OrthancConfiguration::ReaderLock lock;
400 return lock.GetConfiguration().GetBooleanParameter(configuration, true);
401 }
402 }
403
404
405 virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
406 const std::string& remoteAet,
407 const std::string& calledAet)
408 {
409 static const char* configuration = "UnknownSopClassAccepted";
410
411 {
412 std::string lua = "Is" + std::string(configuration);
413
414 LuaScripting::Lock lock(context_.GetLuaScripting());
415
416 if (lock.GetLua().IsExistingFunction(lua.c_str()))
417 {
418 LuaFunctionCall call(lock.GetLua(), lua.c_str());
419 call.PushString(remoteAet);
420 call.PushString(remoteIp);
421 call.PushString(calledAet);
422 return call.ExecutePredicate();
423 }
424 }
425
426 {
427 OrthancConfiguration::ReaderLock lock;
428 return lock.GetConfiguration().GetBooleanParameter(configuration, false);
429 }
430 }
431 };
432
433
434 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
435 {
436 private:
437 ServerContext& context_;
438 OrthancPlugins* plugins_;
439
440 public:
441 MyIncomingHttpRequestFilter(ServerContext& context,
442 OrthancPlugins* plugins) :
443 context_(context),
444 plugins_(plugins)
445 {
446 }
447
448 virtual bool IsAllowed(HttpMethod method,
449 const char* uri,
450 const char* ip,
451 const char* username,
452 const IHttpHandler::Arguments& httpHeaders,
453 const IHttpHandler::GetArguments& getArguments)
454 {
455 #if ORTHANC_ENABLE_PLUGINS == 1
456 if (plugins_ != NULL &&
457 !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
458 {
459 return false;
460 }
461 #endif
462
463 static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
464
465 LuaScripting::Lock lock(context_.GetLuaScripting());
466
467 // Test if the instance must be filtered out
468 if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
469 {
470 LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
471
472 switch (method)
473 {
474 case HttpMethod_Get:
475 call.PushString("GET");
476 break;
477
478 case HttpMethod_Put:
479 call.PushString("PUT");
480 break;
481
482 case HttpMethod_Post:
483 call.PushString("POST");
484 break;
485
486 case HttpMethod_Delete:
487 call.PushString("DELETE");
488 break;
489
490 default:
491 return true;
492 }
493
494 call.PushString(uri);
495 call.PushString(ip);
496 call.PushString(username);
497 call.PushStringMap(httpHeaders);
498
499 if (!call.ExecutePredicate())
500 {
501 LOG(INFO) << "An incoming HTTP request has been discarded by the filter";
502 return false;
503 }
504 }
505
506 return true;
507 }
508 };
509
510
511
512 class MyHttpExceptionFormatter : public IHttpExceptionFormatter
513 {
514 private:
515 bool describeErrors_;
516 OrthancPlugins* plugins_;
517
518 public:
519 MyHttpExceptionFormatter(bool describeErrors,
520 OrthancPlugins* plugins) :
521 describeErrors_(describeErrors),
522 plugins_(plugins)
523 {
524 }
525
526 virtual void Format(HttpOutput& output,
527 const OrthancException& exception,
528 HttpMethod method,
529 const char* uri)
530 {
531 {
532 bool isPlugin = false;
533
534 #if ORTHANC_ENABLE_PLUGINS == 1
535 if (plugins_ != NULL)
536 {
537 plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true);
538 isPlugin = true;
539 }
540 #endif
541
542 if (!isPlugin)
543 {
544 LOG(ERROR) << "Exception in the HTTP handler: " << exception.What();
545 }
546 }
547
548 Json::Value message = Json::objectValue;
549 ErrorCode errorCode = exception.GetErrorCode();
550 HttpStatus httpStatus = exception.GetHttpStatus();
551
552 {
553 bool isPlugin = false;
554
555 #if ORTHANC_ENABLE_PLUGINS == 1
556 if (plugins_ != NULL &&
557 plugins_->GetErrorDictionary().Format(message, httpStatus, exception))
558 {
559 errorCode = ErrorCode_Plugin;
560 isPlugin = true;
561 }
562 #endif
563
564 if (!isPlugin)
565 {
566 message["Message"] = exception.What();
567 }
568 }
569
570 if (!describeErrors_)
571 {
572 output.SendStatus(httpStatus);
573 }
574 else
575 {
576 message["Method"] = EnumerationToString(method);
577 message["Uri"] = uri;
578 message["HttpError"] = EnumerationToString(httpStatus);
579 message["HttpStatus"] = httpStatus;
580 message["OrthancError"] = EnumerationToString(errorCode);
581 message["OrthancStatus"] = errorCode;
582
583 if (exception.HasDetails())
584 {
585 message["Details"] = exception.GetDetails();
586 }
587
588 std::string info = message.toStyledString();
589 output.SendStatus(httpStatus, info);
590 }
591 }
592 };
593
594
595
596 static void PrintHelp(const char* path)
597 {
598 std::cout
599 << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl
600 << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl
601 << std::endl
602 << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl
603 << "case of a directory, all the JSON files it contains will be merged. " << std::endl
604 << "If no configuration path is given on the command line, a set of default " << std::endl
605 << "parameters is used. Please refer to the Orthanc homepage for the full " << std::endl
606 << "instructions about how to use Orthanc <http://www.orthanc-server.com/>." << std::endl
607 << std::endl
608 << "Command-line options:" << std::endl
609 << " --help\t\tdisplay this help and exit" << std::endl
610 << " --logdir=[dir]\tdirectory where to store the log files" << std::endl
611 << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
612 << " --logfile=[file]\tfile where to store the log of Orthanc" << std::endl
613 << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
614 << " --config=[file]\tcreate a sample configuration file and exit" << std::endl
615 << "\t\t\t(if file is \"-\", dumps to stdout)" << std::endl
616 << " --errors\t\tprint the supported error codes and exit" << std::endl
617 << " --verbose\t\tbe verbose in logs" << std::endl
618 << " --trace\t\thighest verbosity in logs (for debug)" << std::endl
619 << " --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl
620 << "\t\t\tdatabase (beware that the database will become" << std::endl
621 << "\t\t\tincompatible with former versions of Orthanc)" << std::endl
622 << " --no-jobs\t\tDon't restart the jobs that were stored during" << std::endl
623 << "\t\t\tthe last execution of Orthanc" << std::endl
624 << " --version\t\toutput version information and exit" << std::endl
625 << std::endl
626 << "Exit status:" << std::endl
627 << " 0 if success," << std::endl
628 #if defined(_WIN32)
629 << "!= 0 if error (use the --errors option to get the list of possible errors)." << std::endl
630 #else
631 << " -1 if error (have a look at the logs)." << std::endl
632 #endif
633 << std::endl;
634 }
635
636
637 static void PrintVersion(const char* path)
638 {
639 std::cout
640 << path << " " << ORTHANC_VERSION << std::endl
641 << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
642 << "Copyright (C) 2017-2020 Osimis S.A. (Belgium)" << std::endl
643 << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>, with OpenSSL exception." << std::endl
644 << "This is free software: you are free to change and redistribute it." << std::endl
645 << "There is NO WARRANTY, to the extent permitted by law." << std::endl
646 << std::endl
647 << "Written by Sebastien Jodogne <s.jodogne@orthanc-labs.com>" << std::endl;
648 }
649
650
651 static void PrintErrorCode(ErrorCode code, const char* description)
652 {
653 std::cout
654 << std::right << std::setw(16)
655 << static_cast<int>(code)
656 << " " << description << std::endl;
657 }
658
659
660 static void PrintErrors(const char* path)
661 {
662 std::cout
663 << path << " " << ORTHANC_VERSION << std::endl
664 << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
665 << std::endl << std::endl
666 << "List of error codes that could be returned by Orthanc:"
667 << std::endl << std::endl;
668
669 // The content of the following brackets is automatically generated
670 // by the "GenerateErrorCodes.py" script
671 {
672 PrintErrorCode(ErrorCode_InternalError, "Internal error");
673 PrintErrorCode(ErrorCode_Success, "Success");
674 PrintErrorCode(ErrorCode_Plugin, "Error encountered within the plugin engine");
675 PrintErrorCode(ErrorCode_NotImplemented, "Not implemented yet");
676 PrintErrorCode(ErrorCode_ParameterOutOfRange, "Parameter out of range");
677 PrintErrorCode(ErrorCode_NotEnoughMemory, "The server hosting Orthanc is running out of memory");
678 PrintErrorCode(ErrorCode_BadParameterType, "Bad type for a parameter");
679 PrintErrorCode(ErrorCode_BadSequenceOfCalls, "Bad sequence of calls");
680 PrintErrorCode(ErrorCode_InexistentItem, "Accessing an inexistent item");
681 PrintErrorCode(ErrorCode_BadRequest, "Bad request");
682 PrintErrorCode(ErrorCode_NetworkProtocol, "Error in the network protocol");
683 PrintErrorCode(ErrorCode_SystemCommand, "Error while calling a system command");
684 PrintErrorCode(ErrorCode_Database, "Error with the database engine");
685 PrintErrorCode(ErrorCode_UriSyntax, "Badly formatted URI");
686 PrintErrorCode(ErrorCode_InexistentFile, "Inexistent file");
687 PrintErrorCode(ErrorCode_CannotWriteFile, "Cannot write to file");
688 PrintErrorCode(ErrorCode_BadFileFormat, "Bad file format");
689 PrintErrorCode(ErrorCode_Timeout, "Timeout");
690 PrintErrorCode(ErrorCode_UnknownResource, "Unknown resource");
691 PrintErrorCode(ErrorCode_IncompatibleDatabaseVersion, "Incompatible version of the database");
692 PrintErrorCode(ErrorCode_FullStorage, "The file storage is full");
693 PrintErrorCode(ErrorCode_CorruptedFile, "Corrupted file (e.g. inconsistent MD5 hash)");
694 PrintErrorCode(ErrorCode_InexistentTag, "Inexistent tag");
695 PrintErrorCode(ErrorCode_ReadOnly, "Cannot modify a read-only data structure");
696 PrintErrorCode(ErrorCode_IncompatibleImageFormat, "Incompatible format of the images");
697 PrintErrorCode(ErrorCode_IncompatibleImageSize, "Incompatible size of the images");
698 PrintErrorCode(ErrorCode_SharedLibrary, "Error while using a shared library (plugin)");
699 PrintErrorCode(ErrorCode_UnknownPluginService, "Plugin invoking an unknown service");
700 PrintErrorCode(ErrorCode_UnknownDicomTag, "Unknown DICOM tag");
701 PrintErrorCode(ErrorCode_BadJson, "Cannot parse a JSON document");
702 PrintErrorCode(ErrorCode_Unauthorized, "Bad credentials were provided to an HTTP request");
703 PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file");
704 PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface");
705 PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area");
706 PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty");
707 PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header");
708 PrintErrorCode(ErrorCode_NullPointer, "Cannot handle a NULL pointer");
709 PrintErrorCode(ErrorCode_DatabaseUnavailable, "The database is currently not available (probably a transient situation)");
710 PrintErrorCode(ErrorCode_CanceledJob, "This job was canceled");
711 PrintErrorCode(ErrorCode_BadGeometry, "Geometry error encountered in Stone");
712 PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates");
713 PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
714 PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
715 PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
716 PrintErrorCode(ErrorCode_SQLiteStatementAlreadyUsed, "SQLite: This cached statement is already being referred to");
717 PrintErrorCode(ErrorCode_SQLiteExecute, "SQLite: Cannot execute a command");
718 PrintErrorCode(ErrorCode_SQLiteRollbackWithoutTransaction, "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)");
719 PrintErrorCode(ErrorCode_SQLiteCommitWithoutTransaction, "SQLite: Committing a nonexistent transaction");
720 PrintErrorCode(ErrorCode_SQLiteRegisterFunction, "SQLite: Unable to register a function");
721 PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
722 PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
723 PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
724 PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
725 PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
726 PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
727 PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
728 PrintErrorCode(ErrorCode_SQLiteTransactionBegin, "SQLite: Cannot start a transaction");
729 PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file");
730 PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage");
731 PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory");
732 PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use");
733 PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is privileged or already in use");
734 PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API");
735 PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file");
736 PrintErrorCode(ErrorCode_PathToExecutable, "Unable to get the path to the executable");
737 PrintErrorCode(ErrorCode_MakeDirectory, "Cannot create a directory");
738 PrintErrorCode(ErrorCode_BadApplicationEntityTitle, "An application entity title (AET) cannot be empty or be longer than 16 characters");
739 PrintErrorCode(ErrorCode_NoCFindHandler, "No request handler factory for DICOM C-FIND SCP");
740 PrintErrorCode(ErrorCode_NoCMoveHandler, "No request handler factory for DICOM C-MOVE SCP");
741 PrintErrorCode(ErrorCode_NoCStoreHandler, "No request handler factory for DICOM C-STORE SCP");
742 PrintErrorCode(ErrorCode_NoApplicationEntityFilter, "No application entity filter");
743 PrintErrorCode(ErrorCode_NoSopClassOrInstance, "DicomUserConnection: Unable to find the SOP class and instance");
744 PrintErrorCode(ErrorCode_NoPresentationContext, "DicomUserConnection: No acceptable presentation context for modality");
745 PrintErrorCode(ErrorCode_DicomFindUnavailable, "DicomUserConnection: The C-FIND command is not supported by the remote SCP");
746 PrintErrorCode(ErrorCode_DicomMoveUnavailable, "DicomUserConnection: The C-MOVE command is not supported by the remote SCP");
747 PrintErrorCode(ErrorCode_CannotStoreInstance, "Cannot store an instance");
748 PrintErrorCode(ErrorCode_CreateDicomNotString, "Only string values are supported when creating DICOM instances");
749 PrintErrorCode(ErrorCode_CreateDicomOverrideTag, "Trying to override a value inherited from a parent module");
750 PrintErrorCode(ErrorCode_CreateDicomUseContent, "Use \"Content\" to inject an image into a new DICOM instance");
751 PrintErrorCode(ErrorCode_CreateDicomNoPayload, "No payload is present for one instance in the series");
752 PrintErrorCode(ErrorCode_CreateDicomUseDataUriScheme, "The payload of the DICOM instance must be specified according to Data URI scheme");
753 PrintErrorCode(ErrorCode_CreateDicomBadParent, "Trying to attach a new DICOM instance to an inexistent resource");
754 PrintErrorCode(ErrorCode_CreateDicomParentIsInstance, "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)");
755 PrintErrorCode(ErrorCode_CreateDicomParentEncoding, "Unable to get the encoding of the parent resource");
756 PrintErrorCode(ErrorCode_UnknownModality, "Unknown modality");
757 PrintErrorCode(ErrorCode_BadJobOrdering, "Bad ordering of filters in a job");
758 PrintErrorCode(ErrorCode_JsonToLuaTable, "Cannot convert the given JSON object to a Lua table");
759 PrintErrorCode(ErrorCode_CannotCreateLua, "Cannot create the Lua context");
760 PrintErrorCode(ErrorCode_CannotExecuteLua, "Cannot execute a Lua command");
761 PrintErrorCode(ErrorCode_LuaAlreadyExecuted, "Arguments cannot be pushed after the Lua function is executed");
762 PrintErrorCode(ErrorCode_LuaBadOutput, "The Lua function does not give the expected number of outputs");
763 PrintErrorCode(ErrorCode_NotLuaPredicate, "The Lua function is not a predicate (only true/false outputs allowed)");
764 PrintErrorCode(ErrorCode_LuaReturnsNoString, "The Lua function does not return a string");
765 PrintErrorCode(ErrorCode_StorageAreaAlreadyRegistered, "Another plugin has already registered a custom storage area");
766 PrintErrorCode(ErrorCode_DatabaseBackendAlreadyRegistered, "Another plugin has already registered a custom database back-end");
767 PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization");
768 PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support");
769 PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series");
770 PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP");
771 PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
772 PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
773 PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
774 PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
775 }
776
777 std::cout << std::endl;
778 }
779
780
781
782 #if ORTHANC_ENABLE_PLUGINS == 1
783 static void LoadPlugins(OrthancPlugins& plugins)
784 {
785 std::list<std::string> path;
786
787 {
788 OrthancConfiguration::ReaderLock lock;
789 lock.GetConfiguration().GetListOfStringsParameter(path, "Plugins");
790 }
791
792 for (std::list<std::string>::const_iterator
793 it = path.begin(); it != path.end(); ++it)
794 {
795 std::string path;
796
797 {
798 OrthancConfiguration::ReaderLock lock;
799 path = lock.GetConfiguration().InterpretStringParameterAsPath(*it);
800 }
801
802 LOG(WARNING) << "Loading plugin(s) from: " << path;
803 plugins.GetManager().RegisterPlugin(path);
804 }
805 }
806 #endif
807
808
809
810 // Returns "true" if restart is required
811 static bool WaitForExit(ServerContext& context,
812 OrthancRestApi& restApi)
813 {
814 LOG(WARNING) << "Orthanc has started";
815
816 #if ORTHANC_ENABLE_PLUGINS == 1
817 if (context.HasPlugins())
818 {
819 context.GetPlugins().SignalOrthancStarted();
820 }
821 #endif
822
823 context.GetLuaScripting().Start();
824 context.GetLuaScripting().Execute("Initialize");
825
826 bool restart;
827
828 for (;;)
829 {
830 ServerBarrierEvent event = SystemToolbox::ServerBarrier(restApi.LeaveBarrierFlag());
831 restart = restApi.IsResetRequestReceived();
832
833 if (!restart &&
834 event == ServerBarrierEvent_Reload)
835 {
836 // Handling of SIGHUP
837
838 OrthancConfiguration::ReaderLock lock;
839 if (lock.GetConfiguration().HasConfigurationChanged())
840 {
841 LOG(WARNING) << "A SIGHUP signal has been received, resetting Orthanc";
842 Logging::Flush();
843 restart = true;
844 break;
845 }
846 else
847 {
848 LOG(WARNING) << "A SIGHUP signal has been received, but is ignored "
849 << "as the configuration has not changed on the disk";
850 Logging::Flush();
851 continue;
852 }
853 }
854 else
855 {
856 break;
857 }
858 }
859
860 context.GetLuaScripting().Execute("Finalize");
861 context.GetLuaScripting().Stop();
862
863 #if ORTHANC_ENABLE_PLUGINS == 1
864 if (context.HasPlugins())
865 {
866 context.GetPlugins().SignalOrthancStopped();
867 }
868 #endif
869
870 if (restart)
871 {
872 LOG(WARNING) << "Reset request received, restarting Orthanc";
873 }
874
875 // We're done
876 LOG(WARNING) << "Orthanc is stopping";
877
878 return restart;
879 }
880
881
882
883 static bool StartHttpServer(ServerContext& context,
884 OrthancRestApi& restApi,
885 OrthancPlugins* plugins)
886 {
887 bool httpServerEnabled;
888
889 {
890 OrthancConfiguration::ReaderLock lock;
891 httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
892 }
893
894 if (!httpServerEnabled)
895 {
896 LOG(WARNING) << "The HTTP server is disabled";
897 return WaitForExit(context, restApi);
898 }
899 else
900 {
901 MyIncomingHttpRequestFilter httpFilter(context, plugins);
902 HttpServer httpServer;
903 bool httpDescribeErrors;
904
905 #if ORTHANC_ENABLE_MONGOOSE == 1
906 const bool defaultKeepAlive = false;
907 #elif ORTHANC_ENABLE_CIVETWEB == 1
908 const bool defaultKeepAlive = true;
909 #else
910 # error "Either Mongoose or Civetweb must be enabled to compile this file"
911 #endif
912
913 {
914 OrthancConfiguration::ReaderLock lock;
915
916 httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
917
918 // HTTP server
919 httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
920 httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
921 httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
922 httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
923 httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
924 httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
925 httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
926
927 // Let's assume that the HTTP server is secure
928 context.SetHttpServerSecure(true);
929
930 bool authenticationEnabled;
931 if (lock.GetConfiguration().LookupBooleanParameter(authenticationEnabled, "AuthenticationEnabled"))
932 {
933 httpServer.SetAuthenticationEnabled(authenticationEnabled);
934
935 if (httpServer.IsRemoteAccessAllowed() &&
936 !authenticationEnabled)
937 {
938 LOG(WARNING) << "====> Remote access is enabled while user authentication is explicitly disabled, "
939 << "your setup is POSSIBLY INSECURE <====";
940 context.SetHttpServerSecure(false);
941 }
942 }
943 else if (httpServer.IsRemoteAccessAllowed())
944 {
945 // Starting with Orthanc 1.5.8, it is impossible to enable
946 // remote access without having explicitly disabled user
947 // authentication.
948 LOG(WARNING) << "Remote access is allowed but \"AuthenticationEnabled\" is not in the configuration, "
949 << "automatically enabling HTTP authentication for security";
950 httpServer.SetAuthenticationEnabled(true);
951 }
952 else
953 {
954 // If Orthanc only listens on the localhost, it is OK to have
955 // "AuthenticationEnabled" disabled
956 httpServer.SetAuthenticationEnabled(false);
957 }
958
959 bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer);
960
961 if (httpServer.IsAuthenticationEnabled() &&
962 !hasUsers)
963 {
964 if (httpServer.IsRemoteAccessAllowed())
965 {
966 /**
967 * Starting with Orthanc 1.5.8, if no user is explicitly
968 * defined while remote access is allowed, we create a
969 * default user, and Orthanc Explorer shows a warning
970 * message about an "Insecure setup". This convention is
971 * used in Docker images "jodogne/orthanc",
972 * "jodogne/orthanc-plugins" and "osimis/orthanc".
973 **/
974 LOG(WARNING) << "====> HTTP authentication is enabled, but no user is declared. "
975 << "Creating a default user: Review your configuration option \"RegisteredUsers\". "
976 << "Your setup is INSECURE <====";
977
978 context.SetHttpServerSecure(false);
979
980 // This is the username/password of the default user in Orthanc.
981 httpServer.RegisterUser("orthanc", "orthanc");
982 }
983 else
984 {
985 LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, "
986 << "check the value of configuration option \"RegisteredUsers\"";
987 }
988 }
989
990 if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
991 {
992 std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
993 lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
994 httpServer.SetSslEnabled(true);
995 httpServer.SetSslCertificate(certificate.c_str());
996 }
997 else
998 {
999 httpServer.SetSslEnabled(false);
1000 }
1001
1002 if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled", false))
1003 {
1004 context.SetExecuteLuaEnabled(true);
1005 LOG(WARNING) << "====> Remote LUA script execution is enabled. Review your configuration option \"ExecuteLuaEnabled\". "
1006 << "Your setup is POSSIBLY INSECURE <====";
1007 }
1008 else
1009 {
1010 context.SetExecuteLuaEnabled(false);
1011 LOG(WARNING) << "Remote LUA script execution is disabled";
1012 }
1013 }
1014
1015 MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
1016
1017 httpServer.SetIncomingHttpRequestFilter(httpFilter);
1018 httpServer.SetHttpExceptionFormatter(exceptionFormatter);
1019 httpServer.Register(context.GetHttpHandler());
1020
1021 if (httpServer.GetPortNumber() < 1024)
1022 {
1023 LOG(WARNING) << "The HTTP port is privileged ("
1024 << httpServer.GetPortNumber() << " is below 1024), "
1025 << "make sure you run Orthanc as root/administrator";
1026 }
1027
1028 httpServer.Start();
1029
1030 bool restart = WaitForExit(context, restApi);
1031
1032 httpServer.Stop();
1033 LOG(WARNING) << " HTTP server has stopped";
1034
1035 return restart;
1036 }
1037 }
1038
1039
1040 static bool StartDicomServer(ServerContext& context,
1041 OrthancRestApi& restApi,
1042 OrthancPlugins* plugins)
1043 {
1044 bool dicomServerEnabled;
1045
1046 {
1047 OrthancConfiguration::ReaderLock lock;
1048 dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
1049 }
1050
1051 if (!dicomServerEnabled)
1052 {
1053 LOG(WARNING) << "The DICOM server is disabled";
1054 return StartHttpServer(context, restApi, plugins);
1055 }
1056 else
1057 {
1058 MyDicomServerFactory serverFactory(context);
1059 OrthancApplicationEntityFilter dicomFilter(context);
1060 ModalitiesFromConfiguration modalities;
1061
1062 // Setup the DICOM server
1063 DicomServer dicomServer;
1064 dicomServer.SetRemoteModalities(modalities);
1065 dicomServer.SetStoreRequestHandlerFactory(serverFactory);
1066 dicomServer.SetMoveRequestHandlerFactory(serverFactory);
1067 dicomServer.SetGetRequestHandlerFactory(serverFactory);
1068 dicomServer.SetFindRequestHandlerFactory(serverFactory);
1069 dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
1070
1071 {
1072 OrthancConfiguration::ReaderLock lock;
1073 dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
1074 dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
1075 dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
1076 dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"));
1077 }
1078
1079 #if ORTHANC_ENABLE_PLUGINS == 1
1080 if (plugins != NULL)
1081 {
1082 if (plugins->HasWorklistHandler())
1083 {
1084 dicomServer.SetWorklistRequestHandlerFactory(*plugins);
1085 }
1086
1087 if (plugins->HasFindHandler())
1088 {
1089 dicomServer.SetFindRequestHandlerFactory(*plugins);
1090 }
1091
1092 if (plugins->HasMoveHandler())
1093 {
1094 dicomServer.SetMoveRequestHandlerFactory(*plugins);
1095 }
1096 }
1097 #endif
1098
1099 dicomServer.SetApplicationEntityFilter(dicomFilter);
1100
1101 if (dicomServer.GetPortNumber() < 1024)
1102 {
1103 LOG(WARNING) << "The DICOM port is privileged ("
1104 << dicomServer.GetPortNumber() << " is below 1024), "
1105 << "make sure you run Orthanc as root/administrator";
1106 }
1107
1108 dicomServer.Start();
1109 LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle()
1110 << " on port: " << dicomServer.GetPortNumber();
1111
1112 bool restart = false;
1113 ErrorCode error = ErrorCode_Success;
1114
1115 try
1116 {
1117 restart = StartHttpServer(context, restApi, plugins);
1118 }
1119 catch (OrthancException& e)
1120 {
1121 error = e.GetErrorCode();
1122 }
1123
1124 dicomServer.Stop();
1125 LOG(WARNING) << " DICOM server has stopped";
1126
1127 serverFactory.Done();
1128
1129 if (error != ErrorCode_Success)
1130 {
1131 throw OrthancException(error);
1132 }
1133
1134 return restart;
1135 }
1136 }
1137
1138
1139 static bool ConfigureHttpHandler(ServerContext& context,
1140 OrthancPlugins *plugins,
1141 bool loadJobsFromDatabase)
1142 {
1143 #if ORTHANC_ENABLE_PLUGINS == 1
1144 // By order of priority, first apply the "plugins" layer, so that
1145 // plugins can overwrite the built-in REST API of Orthanc
1146 if (plugins)
1147 {
1148 assert(context.HasPlugins());
1149 context.GetHttpHandler().Register(*plugins, false);
1150 }
1151 #endif
1152
1153 // Secondly, apply the "static resources" layer
1154 #if ORTHANC_STANDALONE == 1
1155 EmbeddedResourceHttpHandler staticResources("/app", ServerResources::ORTHANC_EXPLORER);
1156 #else
1157 FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
1158 #endif
1159
1160 context.GetHttpHandler().Register(staticResources, false);
1161
1162 // Thirdly, consider the built-in REST API of Orthanc
1163 OrthancRestApi restApi(context);
1164 context.GetHttpHandler().Register(restApi, true);
1165
1166 context.SetupJobsEngine(false /* not running unit tests */, loadJobsFromDatabase);
1167
1168 bool restart = StartDicomServer(context, restApi, plugins);
1169
1170 context.Stop();
1171
1172 return restart;
1173 }
1174
1175
1176 static void UpgradeDatabase(IDatabaseWrapper& database,
1177 IStorageArea& storageArea)
1178 {
1179 // Upgrade the schema of the database, if needed
1180 unsigned int currentVersion = database.GetDatabaseVersion();
1181
1182 LOG(WARNING) << "Starting the upgrade of the database schema";
1183 LOG(WARNING) << "Current database version: " << currentVersion;
1184 LOG(WARNING) << "Database version expected by Orthanc: " << ORTHANC_DATABASE_VERSION;
1185
1186 if (currentVersion == ORTHANC_DATABASE_VERSION)
1187 {
1188 LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
1189 return;
1190 }
1191
1192 if (currentVersion > ORTHANC_DATABASE_VERSION)
1193 {
1194 throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1195 "The version of the database schema (" +
1196 boost::lexical_cast<std::string>(currentVersion) +
1197 ") is too recent for this version of Orthanc. Please upgrade Orthanc.");
1198 }
1199
1200 LOG(WARNING) << "Upgrading the database from schema version "
1201 << currentVersion << " to " << ORTHANC_DATABASE_VERSION;
1202
1203 try
1204 {
1205 database.Upgrade(ORTHANC_DATABASE_VERSION, storageArea);
1206 }
1207 catch (OrthancException&)
1208 {
1209 LOG(ERROR) << "Unable to run the automated upgrade, please use the replication instructions: "
1210 << "http://book.orthanc-server.com/users/replication.html";
1211 throw;
1212 }
1213
1214 // Sanity check
1215 currentVersion = database.GetDatabaseVersion();
1216 if (ORTHANC_DATABASE_VERSION != currentVersion)
1217 {
1218 throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1219 "The database schema was not properly upgraded, it is still at version " +
1220 boost::lexical_cast<std::string>(currentVersion));
1221 }
1222 else
1223 {
1224 LOG(WARNING) << "The database schema was successfully upgraded, "
1225 << "you can now start Orthanc without the \"--upgrade\" argument";
1226 }
1227 }
1228
1229
1230
1231 namespace
1232 {
1233 class ServerContextConfigurator : public boost::noncopyable
1234 {
1235 private:
1236 ServerContext& context_;
1237 OrthancPlugins* plugins_;
1238
1239 public:
1240 ServerContextConfigurator(ServerContext& context,
1241 OrthancPlugins* plugins) :
1242 context_(context),
1243 plugins_(plugins)
1244 {
1245 {
1246 OrthancConfiguration::WriterLock lock;
1247 lock.GetConfiguration().SetServerIndex(context.GetIndex());
1248 }
1249
1250 #if ORTHANC_ENABLE_PLUGINS == 1
1251 if (plugins_ != NULL)
1252 {
1253 plugins_->SetServerContext(context_);
1254 context_.SetPlugins(*plugins_);
1255 }
1256 #endif
1257 }
1258
1259 ~ServerContextConfigurator()
1260 {
1261 {
1262 OrthancConfiguration::WriterLock lock;
1263 lock.GetConfiguration().ResetServerIndex();
1264 }
1265
1266 #if ORTHANC_ENABLE_PLUGINS == 1
1267 if (plugins_ != NULL)
1268 {
1269 plugins_->ResetServerContext();
1270 context_.ResetPlugins();
1271 }
1272 #endif
1273 }
1274 };
1275 }
1276
1277
1278 static bool ConfigureServerContext(IDatabaseWrapper& database,
1279 IStorageArea& storageArea,
1280 OrthancPlugins *plugins,
1281 bool loadJobsFromDatabase)
1282 {
1283 size_t maxCompletedJobs;
1284
1285 {
1286 OrthancConfiguration::ReaderLock lock;
1287
1288 // These configuration options must be set before creating the
1289 // ServerContext, otherwise the possible Lua scripts will not be
1290 // able to properly issue HTTP/HTTPS queries
1291 HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
1292 lock.GetConfiguration().InterpretStringParameterAsPath
1293 (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
1294 HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
1295
1296 // The value "0" below makes the class HttpClient use its default
1297 // value (DEFAULT_HTTP_TIMEOUT = 60 seconds in Orthanc 1.5.7)
1298 HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
1299
1300 HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
1301
1302 DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
1303
1304 maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
1305
1306 if (maxCompletedJobs == 0)
1307 {
1308 LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
1309 }
1310 }
1311
1312 ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
1313
1314 {
1315 OrthancConfiguration::ReaderLock lock;
1316
1317 context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
1318 context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
1319
1320 // New option in Orthanc 1.4.2
1321 context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
1322
1323 try
1324 {
1325 context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
1326 }
1327 catch (...)
1328 {
1329 context.GetIndex().SetMaximumPatientCount(0);
1330 }
1331
1332 try
1333 {
1334 uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
1335 context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
1336 }
1337 catch (...)
1338 {
1339 context.GetIndex().SetMaximumStorageSize(0);
1340 }
1341 }
1342
1343 {
1344 ServerContextConfigurator configurator(context, plugins);
1345
1346 {
1347 OrthancConfiguration::WriterLock lock;
1348 lock.GetConfiguration().LoadModalitiesAndPeers();
1349 }
1350
1351 return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
1352 }
1353 }
1354
1355
1356 static bool ConfigureDatabase(IDatabaseWrapper& database,
1357 IStorageArea& storageArea,
1358 OrthancPlugins *plugins,
1359 bool upgradeDatabase,
1360 bool loadJobsFromDatabase)
1361 {
1362 database.Open();
1363
1364 unsigned int currentVersion = database.GetDatabaseVersion();
1365
1366 if (upgradeDatabase)
1367 {
1368 UpgradeDatabase(database, storageArea);
1369 return false; // Stop and don't restart Orthanc (cf. issue 29)
1370 }
1371 else if (currentVersion != ORTHANC_DATABASE_VERSION)
1372 {
1373 throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1374 "The database schema must be upgraded from version " +
1375 boost::lexical_cast<std::string>(currentVersion) + " to " +
1376 boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
1377 ": Please run Orthanc with the \"--upgrade\" argument");
1378 }
1379
1380 bool success = ConfigureServerContext
1381 (database, storageArea, plugins, loadJobsFromDatabase);
1382
1383 database.Close();
1384
1385 return success;
1386 }
1387
1388
1389 static bool ConfigurePlugins(int argc,
1390 char* argv[],
1391 bool upgradeDatabase,
1392 bool loadJobsFromDatabase)
1393 {
1394 std::unique_ptr<IDatabaseWrapper> databasePtr;
1395 std::unique_ptr<IStorageArea> storage;
1396
1397 #if ORTHANC_ENABLE_PLUGINS == 1
1398 OrthancPlugins plugins;
1399 plugins.SetCommandLineArguments(argc, argv);
1400 LoadPlugins(plugins);
1401
1402 IDatabaseWrapper* database = NULL;
1403 if (plugins.HasDatabaseBackend())
1404 {
1405 LOG(WARNING) << "Using a custom database from plugins";
1406 database = &plugins.GetDatabaseBackend();
1407 }
1408 else
1409 {
1410 databasePtr.reset(CreateDatabaseWrapper());
1411 database = databasePtr.get();
1412 }
1413
1414 if (plugins.HasStorageArea())
1415 {
1416 LOG(WARNING) << "Using a custom storage area from plugins";
1417 storage.reset(plugins.CreateStorageArea());
1418 }
1419 else
1420 {
1421 storage.reset(CreateStorageArea());
1422 }
1423
1424 assert(database != NULL);
1425 assert(storage.get() != NULL);
1426
1427 return ConfigureDatabase(*database, *storage, &plugins,
1428 upgradeDatabase, loadJobsFromDatabase);
1429
1430 #elif ORTHANC_ENABLE_PLUGINS == 0
1431 // The plugins are disabled
1432
1433 databasePtr.reset(CreateDatabaseWrapper());
1434 storage.reset(CreateStorageArea());
1435
1436 assert(databasePtr.get() != NULL);
1437 assert(storage.get() != NULL);
1438
1439 return ConfigureDatabase(*databasePtr, *storage, NULL,
1440 upgradeDatabase, loadJobsFromDatabase);
1441
1442 #else
1443 # error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
1444 #endif
1445 }
1446
1447
1448 static bool StartOrthanc(int argc,
1449 char* argv[],
1450 bool upgradeDatabase,
1451 bool loadJobsFromDatabase)
1452 {
1453 return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase);
1454 }
1455
1456
1457 static bool DisplayPerformanceWarning()
1458 {
1459 (void) DisplayPerformanceWarning; // Disable warning about unused function
1460 LOG(WARNING) << "Performance warning: Non-release build, runtime debug assertions are turned on";
1461 return true;
1462 }
1463
1464
1465 int main(int argc, char* argv[])
1466 {
1467 Logging::Initialize();
1468
1469 bool upgradeDatabase = false;
1470 bool loadJobsFromDatabase = true;
1471 const char* configurationFile = NULL;
1472
1473
1474 /**
1475 * Parse the command-line options.
1476 **/
1477
1478 for (int i = 1; i < argc; i++)
1479 {
1480 std::string argument(argv[i]);
1481
1482 if (argument.empty())
1483 {
1484 // Ignore empty arguments
1485 }
1486 else if (argument[0] != '-')
1487 {
1488 if (configurationFile != NULL)
1489 {
1490 LOG(ERROR) << "More than one configuration path were provided on the command line, aborting";
1491 return -1;
1492 }
1493 else
1494 {
1495 // Use the first argument that does not start with a "-" as
1496 // the configuration file
1497
1498 // TODO WHAT IS THE ENCODING?
1499 configurationFile = argv[i];
1500 }
1501 }
1502 else if (argument == "--errors")
1503 {
1504 PrintErrors(argv[0]);
1505 return 0;
1506 }
1507 else if (argument == "--help")
1508 {
1509 PrintHelp(argv[0]);
1510 return 0;
1511 }
1512 else if (argument == "--version")
1513 {
1514 PrintVersion(argv[0]);
1515 return 0;
1516 }
1517 else if (argument == "--verbose")
1518 {
1519 Logging::EnableInfoLevel(true);
1520 }
1521 else if (argument == "--trace")
1522 {
1523 Logging::EnableTraceLevel(true);
1524 }
1525 else if (boost::starts_with(argument, "--logdir="))
1526 {
1527 // TODO WHAT IS THE ENCODING?
1528 std::string directory = argument.substr(9);
1529
1530 try
1531 {
1532 Logging::SetTargetFolder(directory);
1533 }
1534 catch (OrthancException&)
1535 {
1536 LOG(ERROR) << "The directory where to store the log files ("
1537 << directory << ") is inexistent, aborting.";
1538 return -1;
1539 }
1540 }
1541 else if (boost::starts_with(argument, "--logfile="))
1542 {
1543 // TODO WHAT IS THE ENCODING?
1544 std::string file = argument.substr(10);
1545
1546 try
1547 {
1548 Logging::SetTargetFile(file);
1549 }
1550 catch (OrthancException&)
1551 {
1552 LOG(ERROR) << "Cannot write to the specified log file ("
1553 << file << "), aborting.";
1554 return -1;
1555 }
1556 }
1557 else if (argument == "--upgrade")
1558 {
1559 upgradeDatabase = true;
1560 }
1561 else if (argument == "--no-jobs")
1562 {
1563 loadJobsFromDatabase = false;
1564 }
1565 else if (boost::starts_with(argument, "--config="))
1566 {
1567 // TODO WHAT IS THE ENCODING?
1568 std::string configurationSample;
1569 GetFileResource(configurationSample, ServerResources::CONFIGURATION_SAMPLE);
1570
1571 #if defined(_WIN32)
1572 // Replace UNIX newlines with DOS newlines
1573 boost::replace_all(configurationSample, "\n", "\r\n");
1574 #endif
1575
1576 std::string target = argument.substr(9);
1577
1578 try
1579 {
1580 if (target == "-")
1581 {
1582 // New in 1.5.8: Print to stdout
1583 std::cout << configurationSample;
1584 }
1585 else
1586 {
1587 SystemToolbox::WriteFile(configurationSample, target);
1588 }
1589 return 0;
1590 }
1591 catch (OrthancException&)
1592 {
1593 LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
1594 return -1;
1595 }
1596 }
1597 else
1598 {
1599 LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;
1600 }
1601 }
1602
1603
1604 /**
1605 * Launch Orthanc.
1606 **/
1607
1608 {
1609 std::string version(ORTHANC_VERSION);
1610
1611 if (std::string(ORTHANC_VERSION) == "mainline")
1612 {
1613 try
1614 {
1615 boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
1616 std::time_t creation = boost::filesystem::last_write_time(exe);
1617 boost::posix_time::ptime converted(boost::posix_time::from_time_t(creation));
1618 version += " (" + boost::posix_time::to_iso_string(converted) + ")";
1619 }
1620 catch (...)
1621 {
1622 }
1623 }
1624
1625 LOG(WARNING) << "Orthanc version: " << version;
1626 assert(DisplayPerformanceWarning());
1627
1628 std::string s = "Architecture: ";
1629 if (sizeof(void*) == 4)
1630 {
1631 s += "32-bit, ";
1632 }
1633 else if (sizeof(void*) == 8)
1634 {
1635 s += "64-bit, ";
1636 }
1637 else
1638 {
1639 s += "unsupported pointer size, ";
1640 }
1641
1642 switch (Toolbox::DetectEndianness())
1643 {
1644 case Endianness_Little:
1645 s += "little endian";
1646 break;
1647
1648 case Endianness_Big:
1649 s += "big endian";
1650 break;
1651
1652 default:
1653 s += "unsupported endianness";
1654 break;
1655 }
1656
1657 LOG(INFO) << s;
1658 }
1659
1660 int status = 0;
1661 try
1662 {
1663 for (;;)
1664 {
1665 OrthancInitialize(configurationFile);
1666
1667 bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
1668 if (restart)
1669 {
1670 OrthancFinalize();
1671 LOG(WARNING) << "Logging system is resetting";
1672 Logging::Reset();
1673 }
1674 else
1675 {
1676 break;
1677 }
1678 }
1679 }
1680 catch (const OrthancException& e)
1681 {
1682 LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")";
1683 #if defined(_WIN32)
1684 if (e.GetErrorCode() >= ErrorCode_START_PLUGINS)
1685 {
1686 status = static_cast<int>(ErrorCode_Plugin);
1687 }
1688 else
1689 {
1690 status = static_cast<int>(e.GetErrorCode());
1691 }
1692
1693 #else
1694 status = -1;
1695 #endif
1696 }
1697 catch (const std::exception& e)
1698 {
1699 LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]";
1700 status = -1;
1701 }
1702 catch (const std::string& s)
1703 {
1704 LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]";
1705 status = -1;
1706 }
1707 catch (...)
1708 {
1709 LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any.";
1710 status = -1;
1711 }
1712
1713 LOG(WARNING) << "Orthanc has stopped";
1714
1715 OrthancFinalize();
1716
1717 return status;
1718 }