PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::example::pacs_server_app Class Reference

#include <server_app.h>

Collaboration diagram for kcenon::pacs::example::pacs_server_app:
Collaboration graph

Public Member Functions

 pacs_server_app (const pacs_server_config &config)
 Construct server application with configuration.
 
 ~pacs_server_app ()
 Destructor - stops server if running.
 
 pacs_server_app (const pacs_server_app &)=delete
 
pacs_server_appoperator= (const pacs_server_app &)=delete
 
 pacs_server_app (pacs_server_app &&)=delete
 
pacs_server_appoperator= (pacs_server_app &&)=delete
 
bool initialize ()
 Initialize all components.
 
bool start ()
 Start the DICOM server.
 
void stop ()
 Stop the server gracefully.
 
void wait_for_shutdown ()
 Wait for server shutdown.
 
void request_shutdown ()
 Request shutdown.
 
bool is_running () const noexcept
 Check if server is running.
 
void print_statistics () const
 Get current server statistics.
 

Private Member Functions

bool setup_storage ()
 Set up file storage.
 
bool setup_database ()
 Set up database.
 
bool setup_services ()
 Set up DICOM services.
 
bool setup_server ()
 Set up DICOM server.
 
services::storage_status handle_store (const core::dicom_dataset &dataset, const std::string &calling_ae, const std::string &sop_class_uid, const std::string &sop_instance_uid)
 Handle incoming C-STORE request.
 
std::vector< core::dicom_datasethandle_query (services::query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)
 Handle C-FIND query.
 
std::vector< core::dicom_filehandle_retrieve (const core::dicom_dataset &query_keys)
 Handle C-MOVE/C-GET retrieve.
 
std::vector< core::dicom_datasethandle_worklist_query (const core::dicom_dataset &query_keys, const std::string &calling_ae)
 Handle worklist query.
 
network::Result< std::monostate > handle_mpps_create (const services::mpps_instance &instance)
 Handle MPPS N-CREATE.
 
network::Result< std::monostate > handle_mpps_set (const std::string &sop_instance_uid, const core::dicom_dataset &modifications, services::mpps_status new_status)
 Handle MPPS N-SET.
 

Private Attributes

pacs_server_config config_
 Server configuration.
 
std::unique_ptr< network::dicom_serverserver_
 DICOM server.
 
std::unique_ptr< storage::file_storagefile_storage_
 File storage.
 
std::unique_ptr< storage::index_databasedatabase_
 Index database.
 
std::atomic< bool > shutdown_requested_ {false}
 Shutdown flag.
 
bool initialized_ {false}
 Initialization flag.
 

Detailed Description

Examples
pacs_server/main.cpp.

Definition at line 90 of file server_app.h.

Constructor & Destructor Documentation

◆ pacs_server_app() [1/3]

kcenon::pacs::example::pacs_server_app::pacs_server_app ( const pacs_server_config & config)
explicit

Construct server application with configuration.

Parameters
configServer configuration

Definition at line 56 of file server_app.cpp.

57 : config_(config) {}
pacs_server_config config_
Server configuration.
Definition server_app.h:233

◆ ~pacs_server_app()

kcenon::pacs::example::pacs_server_app::~pacs_server_app ( )

Destructor - stops server if running.

Definition at line 59 of file server_app.cpp.

59 {
60 stop();
61}
void stop()
Stop the server gracefully.

References stop().

Here is the call graph for this function:

◆ pacs_server_app() [2/3]

kcenon::pacs::example::pacs_server_app::pacs_server_app ( const pacs_server_app & )
delete

◆ pacs_server_app() [3/3]

kcenon::pacs::example::pacs_server_app::pacs_server_app ( pacs_server_app && )
delete

Member Function Documentation

◆ handle_mpps_create()

network::Result< std::monostate > kcenon::pacs::example::pacs_server_app::handle_mpps_create ( const services::mpps_instance & instance)
private

Handle MPPS N-CREATE.

Definition at line 659 of file server_app.cpp.

660 {
661
662 std::cout << log_prefix() << "MPPS N-CREATE: " << instance.sop_instance_uid << "\n";
663
664 auto result = database_->create_mpps(
665 instance.sop_instance_uid,
666 instance.station_ae
667 );
668
669 if (result.is_err()) {
670 return kcenon::common::make_error<std::monostate>(1, "MPPS creation failed");
671 }
672
673 return network::Result<std::monostate>(std::monostate{});
674}
std::unique_ptr< storage::index_database > database_
Index database.
Definition server_app.h:242
kcenon::pacs::Result< T > Result
Result type alias using standardized kcenon::pacs::Result<T>
Definition association.h:56

References database_, kcenon::pacs::services::mpps_instance::sop_instance_uid, and kcenon::pacs::services::mpps_instance::station_ae.

Referenced by setup_server().

Here is the caller graph for this function:

◆ handle_mpps_set()

network::Result< std::monostate > kcenon::pacs::example::pacs_server_app::handle_mpps_set ( const std::string & sop_instance_uid,
const core::dicom_dataset & modifications,
services::mpps_status new_status )
private

Handle MPPS N-SET.

Definition at line 676 of file server_app.cpp.

679 {
680
681 std::cout << log_prefix() << "MPPS N-SET: " << sop_instance_uid
682 << " -> " << services::to_string(new_status) << "\n";
683
684 auto result = database_->update_mpps(
685 sop_instance_uid,
686 std::string(services::to_string(new_status))
687 );
688
689 if (result.is_err()) {
690 return kcenon::common::make_error<std::monostate>(1, "MPPS update failed");
691 }
692
693 return network::Result<std::monostate>(std::monostate{});
694}
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60

References database_, and kcenon::pacs::services::to_string().

Referenced by setup_server().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ handle_query()

std::vector< core::dicom_dataset > kcenon::pacs::example::pacs_server_app::handle_query ( services::query_level level,
const core::dicom_dataset & query_keys,
const std::string & calling_ae )
private

Handle C-FIND query.

Definition at line 458 of file server_app.cpp.

461 {
462
463 std::cout << log_prefix() << "C-FIND from " << calling_ae
464 << " at level " << services::to_string(level) << "\n";
465
466 std::vector<core::dicom_dataset> results;
467
468 switch (level) {
470 storage::patient_query query;
471 auto id = query_keys.get_string(core::tags::patient_id);
472 if (!id.empty()) {
473 query.patient_id = id;
474 }
475 auto name = query_keys.get_string(core::tags::patient_name);
476 if (!name.empty()) {
477 query.patient_name = name;
478 }
479
480 auto patients_result = database_->search_patients(query);
481 if (patients_result.is_ok()) {
482 for (const auto& patient : patients_result.value()) {
483 core::dicom_dataset ds;
484 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, patient.patient_id);
485 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, patient.patient_name);
488 results.push_back(std::move(ds));
489 }
490 }
491 break;
492 }
493
495 storage::study_query query;
496 auto id = query_keys.get_string(core::tags::patient_id);
497 if (!id.empty()) {
498 query.patient_id = id;
499 }
500 auto uid = query_keys.get_string(core::tags::study_instance_uid);
501 if (!uid.empty()) {
502 query.study_uid = uid;
503 }
504 auto date = query_keys.get_string(core::tags::study_date);
505 if (!date.empty()) {
506 query.study_date = date;
507 }
508
509 auto studies_result = database_->search_studies(query);
510 if (studies_result.is_ok()) {
511 for (const auto& study : studies_result.value()) {
512 core::dicom_dataset ds;
514 ds.set_string(core::tags::study_date, encoding::vr_type::DA, study.study_date);
515 ds.set_string(core::tags::study_time, encoding::vr_type::TM, study.study_time);
516 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, study.accession_number);
517 ds.set_string(core::tags::study_description, encoding::vr_type::LO, study.study_description);
518 results.push_back(std::move(ds));
519 }
520 }
521 break;
522 }
523
525 storage::series_query query;
526 auto uid = query_keys.get_string(core::tags::study_instance_uid);
527 if (!uid.empty()) {
528 query.study_uid = uid;
529 }
530 auto mod = query_keys.get_string(core::tags::modality);
531 if (!mod.empty()) {
532 query.modality = mod;
533 }
534
535 auto series_result = database_->search_series(query);
536 if (series_result.is_ok()) {
537 for (const auto& series : series_result.value()) {
538 core::dicom_dataset ds;
540 ds.set_string(core::tags::modality, encoding::vr_type::CS, series.modality);
541 if (series.series_number.has_value()) {
543 std::to_string(series.series_number.value()));
544 }
545 ds.set_string(core::tags::series_description, encoding::vr_type::LO, series.series_description);
546 results.push_back(std::move(ds));
547 }
548 }
549 break;
550 }
551
553 storage::instance_query query;
554 auto uid = query_keys.get_string(core::tags::series_instance_uid);
555 if (!uid.empty()) {
556 query.series_uid = uid;
557 }
558
559 auto instances_result = database_->search_instances(query);
560 if (instances_result.is_ok()) {
561 for (const auto& instance : instances_result.value()) {
562 core::dicom_dataset ds;
563 ds.set_string(core::tags::sop_instance_uid, encoding::vr_type::UI, instance.sop_uid);
564 ds.set_string(core::tags::sop_class_uid, encoding::vr_type::UI, instance.sop_class_uid);
565 if (instance.instance_number.has_value()) {
567 std::to_string(instance.instance_number.value()));
568 }
569 results.push_back(std::move(ds));
570 }
571 }
572 break;
573 }
574 }
575
576 std::cout << log_prefix() << " Found " << results.size() << " matches\n";
577 return results;
578}
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag instance_number
Instance Number.
@ DA
Date (8 chars, format: YYYYMMDD)
@ IS
Integer String (12 chars max)
@ LO
Long String (64 chars max)
@ UI
Unique Identifier (64 chars max)
@ PN
Person Name (64 chars max per component group)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ TM
Time (14 chars max, format: HHMMSS.FFFFFF)
@ SH
Short String (16 chars max)
const atna_coded_value query
Query (110112)
@ empty
Z - Replace with zero-length value.
@ id
Implant Displaced (alternate code)
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
std::string_view uid
std::string_view name

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::encoding::CS, kcenon::pacs::encoding::DA, database_, kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::services::image, kcenon::pacs::core::tags::instance_number, kcenon::pacs::encoding::IS, kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::modality, name, kcenon::pacs::services::patient, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::encoding::PN, kcenon::pacs::services::series, kcenon::pacs::core::tags::series_description, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::series_number, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, kcenon::pacs::services::study, kcenon::pacs::core::tags::study_date, kcenon::pacs::core::tags::study_description, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::core::tags::study_time, kcenon::pacs::encoding::TM, kcenon::pacs::services::to_string(), kcenon::pacs::encoding::UI, and uid.

Referenced by setup_server().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ handle_retrieve()

std::vector< core::dicom_file > kcenon::pacs::example::pacs_server_app::handle_retrieve ( const core::dicom_dataset & query_keys)
private

Handle C-MOVE/C-GET retrieve.

Definition at line 580 of file server_app.cpp.

581 {
582
583 std::cout << log_prefix() << "C-MOVE/C-GET retrieve request\n";
584
585 std::vector<core::dicom_file> files;
586 std::vector<std::string> file_paths;
587
588 // Determine query level and get file paths
589 auto sop_uid = query_keys.get_string(core::tags::sop_instance_uid);
590 if (!sop_uid.empty()) {
591 // Instance level
592 auto path_result = database_->get_file_path(sop_uid);
593 if (path_result.is_ok() && path_result.value().has_value()) {
594 file_paths.push_back(path_result.value().value());
595 }
596 } else {
597 auto series_uid = query_keys.get_string(core::tags::series_instance_uid);
598 if (!series_uid.empty()) {
599 // Series level
600 auto series_files_result = database_->get_series_files(series_uid);
601 if (series_files_result.is_ok()) {
602 file_paths = std::move(series_files_result.value());
603 }
604 } else {
605 auto study_uid = query_keys.get_string(core::tags::study_instance_uid);
606 if (!study_uid.empty()) {
607 // Study level
608 auto study_files_result = database_->get_study_files(study_uid);
609 if (study_files_result.is_ok()) {
610 file_paths = std::move(study_files_result.value());
611 }
612 }
613 }
614 }
615
616 // Load files
617 for (const auto& path : file_paths) {
618 auto file_result = core::dicom_file::open(path);
619 if (file_result.is_ok()) {
620 files.push_back(std::move(file_result.value()));
621 }
622 }
623
624 std::cout << log_prefix() << " Found " << files.size() << " files to transfer\n";
625 return files;
626}
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.

References database_, kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::core::dicom_file::open(), kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::sop_instance_uid, and kcenon::pacs::core::tags::study_instance_uid.

Referenced by setup_server().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ handle_store()

services::storage_status kcenon::pacs::example::pacs_server_app::handle_store ( const core::dicom_dataset & dataset,
const std::string & calling_ae,
const std::string & sop_class_uid,
const std::string & sop_instance_uid )
private

Handle incoming C-STORE request.

Definition at line 325 of file server_app.cpp.

329 {
330
331 std::cout << log_prefix() << "C-STORE from " << calling_ae
332 << ": " << sop_instance_uid << "\n";
333
334 // Store to filesystem
335 auto store_result = file_storage_->store(dataset);
336 if (store_result.is_err()) {
337 std::cerr << log_prefix() << "Storage error\n";
339 }
340
341 // Index in database
342 auto patient_id = dataset.get_string(core::tags::patient_id);
343 auto patient_name = dataset.get_string(core::tags::patient_name);
344 auto study_uid = dataset.get_string(core::tags::study_instance_uid);
345 auto series_uid = dataset.get_string(core::tags::series_instance_uid);
346
347 if (patient_id.empty()) {
348 std::cerr << log_prefix() << "Warning: Missing PatientID\n";
350 }
351
352 // Upsert patient and get pk
353 auto birth_date = dataset.get_string(core::tags::patient_birth_date);
354 auto sex = dataset.get_string(core::tags::patient_sex);
355 auto patient_result = database_->upsert_patient(
356 patient_id,
357 patient_name,
358 birth_date,
359 sex
360 );
361 if (patient_result.is_err()) {
362 std::cerr << log_prefix() << "Database error (patient)\n";
364 }
365 int64_t patient_pk = patient_result.value();
366
367 // Upsert study if we have study_uid
368 int64_t study_pk = 0;
369 if (!study_uid.empty()) {
370 auto study_date = dataset.get_string(core::tags::study_date);
371 auto study_time = dataset.get_string(core::tags::study_time);
372 auto accession = dataset.get_string(core::tags::accession_number);
373 auto study_desc = dataset.get_string(core::tags::study_description);
374 auto referring = dataset.get_string(core::tags::referring_physician_name);
375 auto study_id = dataset.get_string(core::tags::study_id);
376
377 auto study_result = database_->upsert_study(
378 patient_pk,
379 study_uid,
380 study_id,
381 study_date,
382 study_time,
383 accession,
384 referring,
385 study_desc
386 );
387 if (study_result.is_err()) {
388 std::cerr << log_prefix() << "Database error (study)\n";
390 }
391 study_pk = study_result.value();
392 }
393
394 // Upsert series if we have series_uid and study_pk
395 int64_t series_pk = 0;
396 if (!series_uid.empty() && study_pk > 0) {
397 auto modality = dataset.get_string(core::tags::modality);
398 auto series_number_str = dataset.get_string(core::tags::series_number);
399 auto series_desc = dataset.get_string(core::tags::series_description);
400
401 std::optional<int> series_number;
402 if (!series_number_str.empty()) {
403 try {
404 series_number = std::stoi(series_number_str);
405 } catch (...) {}
406 }
407
408 auto series_result = database_->upsert_series(
409 study_pk,
410 series_uid,
411 modality,
412 series_number,
413 series_desc,
414 "", // body_part_examined
415 "" // station_name
416 );
417 if (series_result.is_err()) {
418 std::cerr << log_prefix() << "Database error (series)\n";
420 }
421 series_pk = series_result.value();
422 }
423
424 // Upsert instance if we have series_pk
425 if (series_pk > 0) {
426 auto instance_number_str = dataset.get_string(core::tags::instance_number);
427 auto file_path = file_storage_->get_file_path(sop_instance_uid);
428
429 int64_t file_size = 0;
430 if (std::filesystem::exists(file_path)) {
431 file_size = static_cast<int64_t>(std::filesystem::file_size(file_path));
432 }
433
434 std::optional<int> instance_number;
435 if (!instance_number_str.empty()) {
436 try {
437 instance_number = std::stoi(instance_number_str);
438 } catch (...) {}
439 }
440
441 auto instance_result = database_->upsert_instance(
442 series_pk,
443 sop_instance_uid,
444 sop_class_uid,
445 file_path.string(),
446 file_size,
447 "", // transfer_syntax
448 instance_number
449 );
450 if (instance_result.is_err()) {
451 std::cerr << log_prefix() << "Database error (instance)\n";
452 }
453 }
454
456}
std::unique_ptr< storage::file_storage > file_storage_
File storage.
Definition server_app.h:239
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag study_id
Study ID.
@ storage_error
Failure: Unable to process - storage error (0xC001)
@ success
Success - image stored successfully (0x0000)

References kcenon::pacs::core::tags::accession_number, database_, file_storage_, kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::core::tags::instance_number, kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::core::tags::referring_physician_name, kcenon::pacs::core::tags::series_description, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::series_number, kcenon::pacs::services::storage_error, kcenon::pacs::core::tags::study_date, kcenon::pacs::core::tags::study_description, kcenon::pacs::core::tags::study_id, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::core::tags::study_time, and kcenon::pacs::services::success.

Referenced by setup_server().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ handle_worklist_query()

std::vector< core::dicom_dataset > kcenon::pacs::example::pacs_server_app::handle_worklist_query ( const core::dicom_dataset & query_keys,
const std::string & calling_ae )
private

Handle worklist query.

Definition at line 628 of file server_app.cpp.

630 {
631
632 std::cout << log_prefix() << "MWL query from " << calling_ae << "\n";
633
634 std::vector<core::dicom_dataset> results;
635
636 storage::worklist_query query;
637 auto id = query_keys.get_string(core::tags::patient_id);
638 if (!id.empty()) {
639 query.patient_id = id;
640 }
641
642 auto items_result = database_->query_worklist(query);
643
644 if (items_result.is_ok()) {
645 for (const auto& item : items_result.value()) {
646 core::dicom_dataset ds;
647 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, item.patient_id);
648 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, item.patient_name);
649 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, item.accession_no);
651 results.push_back(std::move(ds));
652 }
653 }
654
655 std::cout << log_prefix() << " Found " << results.size() << " worklist items\n";
656 return results;
657}
constexpr dicom_tag item
Item.

References kcenon::pacs::core::tags::accession_number, database_, kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::encoding::PN, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::study_instance_uid, and kcenon::pacs::encoding::UI.

Referenced by setup_server().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ initialize()

bool kcenon::pacs::example::pacs_server_app::initialize ( )
nodiscard

Initialize all components.

Sets up storage, database, and DICOM services. Must be called before start().

Returns
true if initialization succeeded

Definition at line 67 of file server_app.cpp.

67 {
68 std::cout << log_prefix() << "Initializing PACS Server...\n";
69
70 if (!setup_storage()) {
71 return false;
72 }
73
74 if (!setup_database()) {
75 return false;
76 }
77
78 if (!setup_services()) {
79 return false;
80 }
81
82 if (!setup_server()) {
83 return false;
84 }
85
86 initialized_ = true;
87 std::cout << log_prefix() << "PACS Server initialized successfully\n";
88 return true;
89}
bool setup_server()
Set up DICOM server.
bool setup_storage()
Set up file storage.
bool initialized_
Initialization flag.
Definition server_app.h:248
bool setup_services()
Set up DICOM services.
bool setup_database()
Set up database.

References initialized_, setup_database(), setup_server(), setup_services(), and setup_storage().

Referenced by main().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_running()

bool kcenon::pacs::example::pacs_server_app::is_running ( ) const
nodiscardnoexcept

Check if server is running.

Returns
true if server is accepting connections

Definition at line 134 of file server_app.cpp.

134 {
135 return server_ && server_->is_running();
136}
std::unique_ptr< network::dicom_server > server_
DICOM server.
Definition server_app.h:236

References server_.

◆ operator=() [1/2]

pacs_server_app & kcenon::pacs::example::pacs_server_app::operator= ( const pacs_server_app & )
delete

◆ operator=() [2/2]

pacs_server_app & kcenon::pacs::example::pacs_server_app::operator= ( pacs_server_app && )
delete

◆ print_statistics()

void kcenon::pacs::example::pacs_server_app::print_statistics ( ) const

Get current server statistics.

Definition at line 138 of file server_app.cpp.

138 {
139 if (!server_) {
140 return;
141 }
142
143 auto stats = server_->get_statistics();
144 auto uptime = stats.uptime();
145
146 std::cout << "\n";
147 std::cout << "=== PACS Server Statistics ===\n";
148 std::cout << "Uptime: " << uptime.count() << " seconds\n";
149 std::cout << "Total Associations: " << stats.total_associations << "\n";
150 std::cout << "Active Associations: " << stats.active_associations << "\n";
151 std::cout << "Rejected Associations: " << stats.rejected_associations << "\n";
152 std::cout << "Messages Processed: " << stats.messages_processed << "\n";
153 std::cout << "Bytes Received: " << stats.bytes_received << "\n";
154 std::cout << "Bytes Sent: " << stats.bytes_sent << "\n";
155 std::cout << "==============================\n";
156 std::cout << "\n";
157}

References server_.

Referenced by main().

Here is the caller graph for this function:

◆ request_shutdown()

void kcenon::pacs::example::pacs_server_app::request_shutdown ( )

Request shutdown.

Thread-safe method to request server shutdown. Typically called from signal handler.

Definition at line 129 of file server_app.cpp.

129 {
130 shutdown_requested_ = true;
131 stop();
132}
std::atomic< bool > shutdown_requested_
Shutdown flag.
Definition server_app.h:245

References shutdown_requested_, and stop().

Here is the call graph for this function:

◆ setup_database()

bool kcenon::pacs::example::pacs_server_app::setup_database ( )
nodiscardprivate

Set up database.

Definition at line 194 of file server_app.cpp.

194 {
195 std::cout << log_prefix() << "Setting up database...\n";
196 std::cout << log_prefix() << " Path: " << config_.database.path << "\n";
197
198 // Create database directory if needed
199 auto db_dir = config_.database.path.parent_path();
200 if (!db_dir.empty()) {
201 std::error_code ec;
202 std::filesystem::create_directories(db_dir, ec);
203 if (ec) {
204 std::cerr << log_prefix() << "Error: Failed to create database directory: "
205 << ec.message() << "\n";
206 return false;
207 }
208 }
209
210 storage::index_config db_config;
211 db_config.wal_mode = config_.database.wal_mode;
212
213 auto result = storage::index_database::open(
214 config_.database.path.string(), db_config);
215
216 if (result.is_err()) {
217 std::cerr << log_prefix() << "Error: Failed to open database\n";
218 return false;
219 }
220
221 database_ = std::move(result.value());
222 std::cout << log_prefix() << "Database ready\n";
223 return true;
224}
static auto open(std::string_view db_path) -> Result< std::unique_ptr< index_database > >
Open or create a database with default configuration.
bool wal_mode
Enable WAL (Write-Ahead Logging) mode for better concurrency.
Definition config.h:62
std::filesystem::path path
Path to SQLite database file.
Definition config.h:59
database_config database
Database settings.
Definition config.h:98

References config_, kcenon::pacs::example::pacs_server_config::database, database_, kcenon::pacs::storage::index_database::open(), kcenon::pacs::example::database_config::path, kcenon::pacs::example::database_config::wal_mode, and kcenon::pacs::storage::index_config::wal_mode.

Referenced by initialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ setup_server()

bool kcenon::pacs::example::pacs_server_app::setup_server ( )
nodiscardprivate

Set up DICOM server.

Definition at line 238 of file server_app.cpp.

238 {
239 std::cout << log_prefix() << "Setting up DICOM server...\n";
240
241 network::server_config server_config;
242 server_config.ae_title = config_.server.ae_title;
243 server_config.port = config_.server.port;
244 server_config.max_associations = config_.server.max_associations;
245 server_config.idle_timeout = config_.server.idle_timeout;
246
247 // Set up AE whitelist if configured
249 server_config.ae_whitelist = config_.access_control.allowed_ae_titles;
250 server_config.accept_unknown_calling_ae = false;
251 }
252
253 server_ = std::make_unique<network::dicom_server>(server_config);
254
255 // Register Verification SCP
256 server_->register_service(std::make_shared<services::verification_scp>());
257
258 // Register Storage SCP
259 auto storage_scp = std::make_shared<services::storage_scp>();
260 storage_scp->set_handler(
261 [this](const auto& ds, const auto& ae, const auto& sop_class, const auto& sop_uid) {
262 return handle_store(ds, ae, sop_class, sop_uid);
263 });
264 server_->register_service(storage_scp);
265
266 // Register Query SCP
267 auto query_scp = std::make_shared<services::query_scp>();
268 query_scp->set_handler(
269 [this](auto level, const auto& keys, const auto& ae) {
270 return handle_query(level, keys, ae);
271 });
272 server_->register_service(query_scp);
273
274 // Register Retrieve SCP
275 auto retrieve_scp = std::make_shared<services::retrieve_scp>();
276 retrieve_scp->set_retrieve_handler(
277 [this](const auto& keys) {
278 return handle_retrieve(keys);
279 });
280 server_->register_service(retrieve_scp);
281
282 // Register Worklist SCP
283 auto worklist_scp = std::make_shared<services::worklist_scp>();
284 worklist_scp->set_handler(
285 [this](const auto& keys, const auto& ae) {
286 return handle_worklist_query(keys, ae);
287 });
288 server_->register_service(worklist_scp);
289
290 // Register MPPS SCP
291 auto mpps_scp = std::make_shared<services::mpps_scp>();
292 mpps_scp->set_create_handler(
293 [this](const auto& instance) {
294 return handle_mpps_create(instance);
295 });
296 mpps_scp->set_set_handler(
297 [this](const auto& uid, const auto& mods, auto status) {
298 return handle_mpps_set(uid, mods, status);
299 });
300 server_->register_service(mpps_scp);
301
302 // Set up callbacks
303 server_->on_association_established([](const network::association& assoc) {
304 std::cout << log_prefix() << "Association established: "
305 << assoc.calling_ae() << " -> " << assoc.called_ae() << "\n";
306 });
307
308 server_->on_association_released([](const network::association& assoc) {
309 std::cout << log_prefix() << "Association released: "
310 << assoc.calling_ae() << "\n";
311 });
312
313 server_->on_error([](const std::string& error) {
314 std::cerr << log_prefix() << "Server error: " << error << "\n";
315 });
316
317 std::cout << log_prefix() << "DICOM server configured\n";
318 return true;
319}
std::vector< core::dicom_file > handle_retrieve(const core::dicom_dataset &query_keys)
Handle C-MOVE/C-GET retrieve.
network::Result< std::monostate > handle_mpps_create(const services::mpps_instance &instance)
Handle MPPS N-CREATE.
std::vector< core::dicom_dataset > handle_worklist_query(const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle worklist query.
services::storage_status handle_store(const core::dicom_dataset &dataset, const std::string &calling_ae, const std::string &sop_class_uid, const std::string &sop_instance_uid)
Handle incoming C-STORE request.
network::Result< std::monostate > handle_mpps_set(const std::string &sop_instance_uid, const core::dicom_dataset &modifications, services::mpps_status new_status)
Handle MPPS N-SET.
std::vector< core::dicom_dataset > handle_query(services::query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle C-FIND query.
@ error
Node returned an error.
std::vector< std::string > allowed_ae_titles
Allowed AE titles (empty = accept all)
Definition config.h:84
access_control_config access_control
Access control settings.
Definition config.h:104
server_network_config server
Server network settings.
Definition config.h:92
uint16_t port
Port to listen on.
Definition config.h:31
std::chrono::seconds idle_timeout
Idle timeout for associations in seconds (0 = no timeout)
Definition config.h:37
std::string ae_title
Application Entity Title for this server (max 16 chars)
Definition config.h:28
size_t max_associations
Maximum concurrent associations (0 = unlimited)
Definition config.h:34

References kcenon::pacs::example::pacs_server_config::access_control, kcenon::pacs::example::server_network_config::ae_title, kcenon::pacs::network::server_config::ae_title, kcenon::pacs::example::access_control_config::allowed_ae_titles, kcenon::pacs::network::association::called_ae(), kcenon::pacs::network::association::calling_ae(), config_, handle_mpps_create(), handle_mpps_set(), handle_query(), handle_retrieve(), handle_store(), handle_worklist_query(), kcenon::pacs::example::server_network_config::idle_timeout, kcenon::pacs::example::server_network_config::max_associations, kcenon::pacs::example::server_network_config::port, kcenon::pacs::example::pacs_server_config::server, server_, and uid.

Referenced by initialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ setup_services()

bool kcenon::pacs::example::pacs_server_app::setup_services ( )
nodiscardprivate

Set up DICOM services.

Definition at line 226 of file server_app.cpp.

226 {
227 std::cout << log_prefix() << "Setting up DICOM services...\n";
228 std::cout << log_prefix() << " - Verification SCP (C-ECHO)\n";
229 std::cout << log_prefix() << " - Storage SCP (C-STORE)\n";
230 std::cout << log_prefix() << " - Query SCP (C-FIND)\n";
231 std::cout << log_prefix() << " - Retrieve SCP (C-MOVE/C-GET)\n";
232 std::cout << log_prefix() << " - Worklist SCP (MWL)\n";
233 std::cout << log_prefix() << " - MPPS SCP (N-CREATE/N-SET)\n";
234 std::cout << log_prefix() << "All DICOM services configured\n";
235 return true;
236}

Referenced by initialize().

Here is the caller graph for this function:

◆ setup_storage()

bool kcenon::pacs::example::pacs_server_app::setup_storage ( )
nodiscardprivate

Set up file storage.

Definition at line 163 of file server_app.cpp.

163 {
164 std::cout << log_prefix() << "Setting up file storage...\n";
165 std::cout << log_prefix() << " Directory: " << config_.storage.directory << "\n";
166
167 // Create storage directory if it doesn't exist
168 std::error_code ec;
169 std::filesystem::create_directories(config_.storage.directory, ec);
170 if (ec) {
171 std::cerr << log_prefix() << "Error: Failed to create storage directory: "
172 << ec.message() << "\n";
173 return false;
174 }
175
176 storage::file_storage_config storage_config;
177 storage_config.root_path = config_.storage.directory;
178 storage_config.naming = parse_naming_scheme(config_.storage.naming);
179 storage_config.duplicate = parse_duplicate_policy(config_.storage.duplicate_policy);
180 storage_config.create_directories = true;
181
182 try {
183 file_storage_ = std::make_unique<storage::file_storage>(storage_config);
184 } catch (const std::exception& e) {
185 std::cerr << log_prefix() << "Error: Failed to create file storage: "
186 << e.what() << "\n";
187 return false;
188 }
189
190 std::cout << log_prefix() << "File storage ready\n";
191 return true;
192}
storage_config storage
Storage settings.
Definition config.h:95
std::filesystem::path directory
Root directory for DICOM file storage.
Definition config.h:45
std::string duplicate_policy
Duplicate handling policy: "reject", "replace", "ignore".
Definition config.h:51
std::string naming
File naming scheme: "hierarchical" or "flat".
Definition config.h:48

References config_, kcenon::pacs::example::storage_config::directory, kcenon::pacs::example::storage_config::duplicate_policy, file_storage_, kcenon::pacs::example::storage_config::naming, and kcenon::pacs::example::pacs_server_config::storage.

Referenced by initialize().

Here is the caller graph for this function:

◆ start()

bool kcenon::pacs::example::pacs_server_app::start ( )
nodiscard

Start the DICOM server.

Begins accepting connections on the configured port. initialize() must be called before start().

Returns
true if server started successfully

Definition at line 91 of file server_app.cpp.

91 {
92 if (!initialized_) {
93 std::cerr << log_prefix() << "Error: Server not initialized\n";
94 return false;
95 }
96
97 std::cout << log_prefix() << "Starting DICOM server...\n";
98 std::cout << log_prefix() << " AE Title: " << config_.server.ae_title << "\n";
99 std::cout << log_prefix() << " Port: " << config_.server.port << "\n";
100 std::cout << log_prefix() << " Max Associations: " << config_.server.max_associations << "\n";
101
102 auto result = server_->start();
103 if (result.is_err()) {
104 std::cerr << log_prefix() << "Error: Failed to start server\n";
105 return false;
106 }
107
108 std::cout << log_prefix() << "PACS Server started successfully\n";
109 std::cout << log_prefix() << "Listening on port " << config_.server.port << "...\n";
110 std::cout << log_prefix() << "Press Ctrl+C to stop\n";
111
112 return true;
113}

References kcenon::pacs::example::server_network_config::ae_title, config_, initialized_, kcenon::pacs::example::server_network_config::max_associations, kcenon::pacs::example::server_network_config::port, kcenon::pacs::example::pacs_server_config::server, and server_.

Referenced by main().

Here is the caller graph for this function:

◆ stop()

void kcenon::pacs::example::pacs_server_app::stop ( )

Stop the server gracefully.

Stops accepting new connections and waits for active associations to complete.

Definition at line 115 of file server_app.cpp.

115 {
116 if (server_ && server_->is_running()) {
117 std::cout << log_prefix() << "Stopping DICOM server...\n";
118 server_->stop();
119 std::cout << log_prefix() << "DICOM server stopped\n";
120 }
121}

References server_.

Referenced by request_shutdown(), and ~pacs_server_app().

Here is the caller graph for this function:

◆ wait_for_shutdown()

void kcenon::pacs::example::pacs_server_app::wait_for_shutdown ( )

Wait for server shutdown.

Blocks until the server is stopped.

Definition at line 123 of file server_app.cpp.

123 {
124 if (server_) {
125 server_->wait_for_shutdown();
126 }
127}

References server_.

Referenced by main().

Here is the caller graph for this function:

Member Data Documentation

◆ config_

pacs_server_config kcenon::pacs::example::pacs_server_app::config_
private

Server configuration.

Definition at line 233 of file server_app.h.

Referenced by setup_database(), setup_server(), setup_storage(), and start().

◆ database_

std::unique_ptr<storage::index_database> kcenon::pacs::example::pacs_server_app::database_
private

◆ file_storage_

std::unique_ptr<storage::file_storage> kcenon::pacs::example::pacs_server_app::file_storage_
private

File storage.

Definition at line 239 of file server_app.h.

Referenced by handle_store(), and setup_storage().

◆ initialized_

bool kcenon::pacs::example::pacs_server_app::initialized_ {false}
private

Initialization flag.

Definition at line 248 of file server_app.h.

248{false};

Referenced by initialize(), and start().

◆ server_

std::unique_ptr<network::dicom_server> kcenon::pacs::example::pacs_server_app::server_
private

DICOM server.

Definition at line 236 of file server_app.h.

Referenced by is_running(), print_statistics(), setup_server(), start(), stop(), and wait_for_shutdown().

◆ shutdown_requested_

std::atomic<bool> kcenon::pacs::example::pacs_server_app::shutdown_requested_ {false}
private

Shutdown flag.

Definition at line 245 of file server_app.h.

245{false};

Referenced by request_shutdown().


The documentation for this class was generated from the following files: