PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
dicom_server_v2.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <algorithm>
15#include <sstream>
16#include <thread>
17
18// KCENON_HAS_COMMON_SYSTEM is defined by CMake when common_system is available
19#ifndef KCENON_HAS_COMMON_SYSTEM
20#define KCENON_HAS_COMMON_SYSTEM 0
21#endif
22
23#ifdef PACS_WITH_NETWORK_SYSTEM
24#include <kcenon/network/facade/tcp_facade.h>
25#endif
26
27#if KCENON_HAS_COMMON_SYSTEM
28using kcenon::common::error_info;
29#endif
30
32
33// =============================================================================
34// Construction / Destruction
35// =============================================================================
36
38 : config_(config) {
39 stats_.start_time = clock::now();
41}
42
44 try {
45 if (running_) {
46 stop();
47 }
48 } catch (...) {
49 // Suppress exceptions in destructor to prevent std::terminate
50 }
51}
52
53// =============================================================================
54// Service Registration
55// =============================================================================
56
58 if (!service) {
59 return;
60 }
61
62 std::lock_guard<std::mutex> lock(services_mutex_);
63
64 // Register SOP Class mappings
65 for (const auto& sop_class : service->supported_sop_classes()) {
66 sop_class_to_service_[sop_class] = service.get();
67 }
68
69 services_.push_back(std::move(service));
70}
71
72std::vector<std::string> dicom_server_v2::supported_sop_classes() const {
73 std::lock_guard<std::mutex> lock(services_mutex_);
74
75 std::vector<std::string> sop_classes;
76 sop_classes.reserve(sop_class_to_service_.size());
77
78 for (const auto& [uid, _] : sop_class_to_service_) {
79 sop_classes.push_back(uid);
80 }
81
82 return sop_classes;
83}
84
85// =============================================================================
86// Lifecycle Management
87// =============================================================================
88
90 if (running_.exchange(true)) {
91 return error_info("Server already running");
92 }
93
94 // Validate configuration
95 if (config_.ae_title.empty()) {
96 running_ = false;
97 return error_info("AE Title cannot be empty");
98 }
99
100 if (config_.ae_title.length() > AE_TITLE_LENGTH) {
101 running_ = false;
102 return error_info("AE Title exceeds 16 characters");
103 }
104
105 if (config_.port == 0) {
106 running_ = false;
107 return error_info("Invalid port number");
108 }
109
110 // Check that at least one service is registered
111 {
112 std::lock_guard<std::mutex> lock(services_mutex_);
113 if (services_.empty()) {
114 running_ = false;
115 return error_info("No services registered");
116 }
117 }
118
119 // Reset statistics
120 {
121 std::lock_guard<std::mutex> lock(stats_mutex_);
122 stats_.start_time = clock::now();
129 stats_.bytes_sent = 0;
130 }
131
132#ifdef PACS_WITH_NETWORK_SYSTEM
133 try {
134 // Create TCP server via tcp_facade
135 kcenon::network::facade::tcp_facade facade;
136 kcenon::network::facade::tcp_facade::server_config srv_cfg;
137 srv_cfg.server_id = config_.ae_title;
138 srv_cfg.port = config_.port;
139 // Handle both old API (returns shared_ptr) and new API (returns Result<shared_ptr>)
140 server_ = [](auto raw) -> std::shared_ptr<kcenon::network::interfaces::i_protocol_server> {
141 if constexpr (requires { raw.is_err(); raw.value(); }) {
142 if (raw.is_err()) throw std::runtime_error("Failed to create server");
143 return std::move(raw.value());
144 } else {
145 return std::move(raw);
146 }
147 }(facade.create_server(srv_cfg));
148
149 // Set up server-level callbacks
150 server_->set_connection_callback(
151 [this](std::shared_ptr<kcenon::network::interfaces::i_session> session) {
152 on_connection(std::move(session));
153 });
154
155 server_->set_disconnection_callback(
156 [this](std::string_view session_id) {
157 on_disconnection(session_id);
158 });
159
160 server_->set_receive_callback(
161 [this](std::string_view session_id,
162 const std::vector<uint8_t>& data) {
163 on_receive(session_id, data);
164 });
165
166 server_->set_error_callback(
167 [this](std::string_view session_id,
168 std::error_code ec) {
169 on_network_error(session_id, ec);
170 });
171
172 // Start the server on configured port
173 auto result = server_->start(config_.port);
174 if (result.is_err()) {
175 running_ = false;
176 server_.reset();
177 return error_info("Failed to start server");
178 }
179
180 return std::monostate{};
181 } catch (const std::exception& e) {
182 running_ = false;
183 if (server_) {
184 server_.reset();
185 }
186 return error_info(std::string("Exception during server start: ") + e.what());
187 } catch (...) {
188 running_ = false;
189 if (server_) {
190 server_.reset();
191 }
192 return error_info("Unknown exception during server start");
193 }
194#else
195 running_ = false;
196 return error_info("dicom_server_v2 requires PACS_WITH_NETWORK_SYSTEM");
197#endif
198}
199
201 if (!running_.exchange(false)) {
202 return; // Already stopped
203 }
204
205#ifdef PACS_WITH_NETWORK_SYSTEM
206 // Phase 1: Stop accepting new connections
207 if (server_) {
208 try {
209 // Stop the server - this closes the acceptor
210 (void)server_->stop();
211 } catch (...) {
212 // Suppress exceptions during server stop
213 }
214 }
215
216 // Phase 2: Wait for handlers to complete gracefully
217 auto deadline = clock::now() + timeout;
218 {
219 std::unique_lock<std::mutex> lock(handlers_mutex_);
220 while (!handlers_.empty() && clock::now() < deadline) {
221 // Release lock while waiting
222 lock.unlock();
223 std::this_thread::sleep_for(std::chrono::milliseconds{100});
224 lock.lock();
225 }
226 }
227
228 // Phase 3: Force stop remaining handlers
229 // Collect handlers first, then stop them without holding the lock
230 // to avoid potential deadlock if handler->stop() triggers callbacks
231 std::vector<std::shared_ptr<dicom_association_handler>> handlers_to_stop;
232 {
233 std::lock_guard<std::mutex> lock(handlers_mutex_);
234 handlers_to_stop.reserve(handlers_.size());
235 for (auto& [session_id, handler] : handlers_) {
236 handlers_to_stop.push_back(handler);
237 }
238 handlers_.clear();
239 }
240
241 // Stop handlers without holding the lock
242 for (auto& handler : handlers_to_stop) {
243 try {
244 handler->stop(false); // Force abort
245 } catch (...) {
246 // Suppress exceptions during handler stop
247 }
248 }
249 handlers_to_stop.clear();
250
251 // Allow any pending callbacks to complete
252 std::this_thread::sleep_for(std::chrono::milliseconds{50});
253
254 // Clear callbacks to break reference cycles before server is destroyed.
255 // The server object itself is kept alive until dicom_server_v2 is destroyed
256 // to avoid use-after-free in the adapter's background I/O threads.
257 if (server_) {
258 server_->set_connection_callback(nullptr);
259 server_->set_disconnection_callback(nullptr);
260 server_->set_receive_callback(nullptr);
261 server_->set_error_callback(nullptr);
262 }
263#else
264 (void)timeout; // Unused without network_system
265#endif
266
267 // Notify shutdown waiters
268 {
269 std::lock_guard<std::mutex> lock(shutdown_mutex_);
270 shutdown_cv_.notify_all();
271 }
272}
273
275 std::unique_lock<std::mutex> lock(shutdown_mutex_);
276 shutdown_cv_.wait(lock, [this]() { return !running_; });
277}
278
279// =============================================================================
280// Status Queries
281// =============================================================================
282
283bool dicom_server_v2::is_running() const noexcept {
284 return running_;
285}
286
288 std::lock_guard<std::mutex> lock(handlers_mutex_);
289 return handlers_.size();
290}
291
293 std::lock_guard<std::mutex> lock(stats_mutex_);
294 server_statistics result = stats_;
296 return result;
297}
298
299const server_config& dicom_server_v2::config() const noexcept {
300 return config_;
301}
302
303// =============================================================================
304// Callbacks
305// =============================================================================
306
308 std::lock_guard<std::mutex> lock(callback_mutex_);
309 on_established_cb_ = std::move(callback);
310}
311
313 std::lock_guard<std::mutex> lock(callback_mutex_);
314 on_closed_cb_ = std::move(callback);
315}
316
318 std::lock_guard<std::mutex> lock(callback_mutex_);
319 on_error_cb_ = std::move(callback);
320}
321
322// =============================================================================
323// Network System Callbacks
324// =============================================================================
325
327 std::shared_ptr<kcenon::network::interfaces::i_session> session) {
328
329 if (!running_ || !session) {
330 return;
331 }
332
333 // Check max associations limit
334 {
335 std::lock_guard<std::mutex> lock(handlers_mutex_);
336 if (config_.max_associations > 0 &&
338 // Reject due to resource limit
339 report_error("Max associations limit reached, rejecting connection");
340
341 std::lock_guard<std::mutex> stats_lock(stats_mutex_);
343
344#ifdef PACS_WITH_NETWORK_SYSTEM
345 session->close();
346#endif
347 return;
348 }
349 }
350
351 // Create handler for this session
352 create_handler(std::move(session));
353}
354
355void dicom_server_v2::on_disconnection(std::string_view session_id) {
356 // Skip if server is shutting down
357 if (!running_) {
358 return;
359 }
360
361 std::string sid(session_id);
362
363 // Notify handler of disconnection before removing it
364 auto handler = find_handler(sid);
365 if (handler) {
366 handler->handle_disconnect();
367 }
368
369 // Remove handler for this session
370 remove_handler(sid);
371}
372
374 std::string_view session_id,
375 const std::vector<uint8_t>& data) {
376
377 std::string sid(session_id);
378
379 // Find handler and forward data
380 auto handler = find_handler(sid);
381 if (handler) {
382 // Forward data to handler for PDU processing
383 handler->feed_data(data);
384
385 // Update statistics
386 {
387 std::lock_guard<std::mutex> lock(stats_mutex_);
388 stats_.bytes_received += data.size();
389 stats_.last_activity = clock::now();
390 }
391 }
392}
393
395 std::string_view session_id,
396 std::error_code ec) {
397
398 // Skip if server is shutting down
399 if (!running_) {
400 return;
401 }
402
403 std::string sid(session_id);
404
405 std::ostringstream oss;
406 oss << "Network error on session " << sid
407 << ": " << ec.message() << " (" << ec.value() << ")";
408 report_error(oss.str());
409
410 // Notify handler of the error
411 auto handler = find_handler(sid);
412 if (handler) {
413 handler->handle_error(ec);
414 }
415
416 // Remove the handler - it will clean itself up
417 remove_handler(sid);
418}
419
420// =============================================================================
421// Handler Management
422// =============================================================================
423
425 std::shared_ptr<kcenon::network::interfaces::i_session> session) {
426
427#ifdef PACS_WITH_NETWORK_SYSTEM
428 const std::string session_id(session->id());
429#else
430 const std::string session_id;
431 (void)session;
432#endif
433
434 // Build service map for handler
435 auto service_map = build_service_map();
436
437 // Create handler
438 auto handler = std::make_shared<dicom_association_handler>(
439 std::move(session), config_, service_map);
440
441 // Set up access control if configured
442 {
443 std::lock_guard<std::mutex> acl_lock(acl_mutex_);
444 if (access_control_) {
445 handler->set_access_control(access_control_);
446 handler->set_access_control_enabled(access_control_enabled_);
447 }
448 }
449
450 // Set up handler callbacks
451 auto weak_this = std::weak_ptr<dicom_server_v2*>(
452 std::shared_ptr<dicom_server_v2*>(nullptr, [](dicom_server_v2**) {}));
453 // Note: We can't use shared_from_this() since dicom_server_v2 doesn't inherit
454 // from enable_shared_from_this. Instead, capture 'this' directly since the
455 // handler's lifetime is bounded by the server's lifetime.
456
457 handler->set_established_callback(
458 [this](const std::string& sid, const std::string& calling_ae,
459 const std::string& called_ae) {
460 // Update statistics
461 {
462 std::lock_guard<std::mutex> lock(stats_mutex_);
464 stats_.last_activity = clock::now();
465 }
466
467 // Forward to user callback
468 {
469 std::lock_guard<std::mutex> lock(callback_mutex_);
470 if (on_established_cb_) {
471 on_established_cb_(sid, calling_ae, called_ae);
472 }
473 }
474 });
475
476 handler->set_closed_callback(
477 [this](const std::string& sid, bool graceful) {
478 // Forward to user callback
479 {
480 std::lock_guard<std::mutex> lock(callback_mutex_);
481 if (on_closed_cb_) {
482 on_closed_cb_(sid, graceful);
483 }
484 }
485
486 // Remove handler from map
487 remove_handler(sid);
488 });
489
490 handler->set_error_callback(
491 [this](const std::string& /*sid*/, const std::string& error) {
492 report_error(error);
493 });
494
495 // Register handler
496 {
497 std::lock_guard<std::mutex> lock(handlers_mutex_);
498 handlers_[session_id] = handler;
499 }
500
501 // Start handler (begins processing PDUs)
502 handler->start();
503}
504
505void dicom_server_v2::remove_handler(const std::string& session_id) {
506 // Skip if server is shutting down - handlers are cleaned up in stop()
507 if (!running_) {
508 return;
509 }
510
511 std::lock_guard<std::mutex> lock(handlers_mutex_);
512 auto it = handlers_.find(session_id);
513 if (it != handlers_.end()) {
514 // Handler cleans itself up via stop() in destructor
515 handlers_.erase(it);
516 }
517}
518
519std::shared_ptr<dicom_association_handler>
520dicom_server_v2::find_handler(const std::string& session_id) const {
521 std::lock_guard<std::mutex> lock(handlers_mutex_);
522 auto it = handlers_.find(session_id);
523 if (it != handlers_.end()) {
524 return it->second;
525 }
526 return nullptr;
527}
528
530 if (config_.idle_timeout.count() == 0) {
531 return; // No timeout configured
532 }
533
534 auto now = clock::now();
535 std::vector<std::string> timed_out;
536
537 {
538 std::lock_guard<std::mutex> lock(handlers_mutex_);
539 for (const auto& [session_id, handler] : handlers_) {
540 auto idle_duration = std::chrono::duration_cast<std::chrono::seconds>(
541 now - handler->last_activity());
542
543 if (idle_duration >= config_.idle_timeout) {
544 timed_out.push_back(session_id);
545 }
546 }
547 }
548
549 // Stop timed-out handlers
550 for (const auto& session_id : timed_out) {
551 auto handler = find_handler(session_id);
552 if (handler) {
553 handler->stop(false); // Force abort
554 }
555 }
556}
557
558// =============================================================================
559// Internal Helpers
560// =============================================================================
561
566
567void dicom_server_v2::report_error(const std::string& error) {
568 std::lock_guard<std::mutex> lock(callback_mutex_);
569 if (on_error_cb_) {
570 on_error_cb_(error);
571 }
572}
573
574// =============================================================================
575// Security / Access Control
576// =============================================================================
577
579 std::shared_ptr<security::access_control_manager> acm) {
580 std::lock_guard<std::mutex> lock(acl_mutex_);
581 access_control_ = std::move(acm);
582 if (access_control_) {
584 }
585}
586
587std::shared_ptr<security::access_control_manager>
589 std::lock_guard<std::mutex> lock(acl_mutex_);
590 return access_control_;
591}
592
596
600
601} // namespace kcenon::pacs::network::v2
std::map< std::string, services::scp_service * > service_map
DICOM server using network_system's messaging_server for connection management.
void on_network_error(std::string_view session_id, std::error_code ec)
Handle network error.
server_config config_
Server configuration.
std::mutex acl_mutex_
Access control mutex.
void report_error(const std::string &error)
Report error through callback.
dicom_server_v2(const server_config &config)
Construct server with configuration.
std::atomic< bool > running_
Running flag.
server_statistics get_statistics() const
Get server statistics.
std::vector< services::scp_service_ptr > services_
Registered SCP services.
void on_error(error_callback callback)
Set callback for error events.
association_closed_callback on_closed_cb_
Association closed callback.
const server_config & config() const noexcept
Get server configuration.
std::shared_ptr< security::access_control_manager > access_control_
Access control manager for RBAC.
void create_handler(std::shared_ptr< kcenon::network::interfaces::i_session > session)
Create and register a new handler for a session.
std::unordered_map< std::string, std::shared_ptr< dicom_association_handler > > handlers_
Active association handlers (keyed by session ID)
void on_receive(std::string_view session_id, const std::vector< uint8_t > &data)
Handle receive data (forwarded to handler)
std::shared_ptr< kcenon::network::interfaces::i_protocol_server > server_
network_system's protocol server (via tcp_facade)
bool is_running() const noexcept
Check if server is running.
void register_service(services::scp_service_ptr service)
Register an SCP service.
dicom_association_handler::service_map build_service_map() const
Build service map from registered services.
void on_association_established(association_established_callback callback)
Set callback for association established events.
std::function< void(const std::string &session_id, bool graceful)> association_closed_callback
Callback type for association closed events.
association_established_callback on_established_cb_
Association established callback.
void wait_for_shutdown()
Wait for server shutdown.
std::mutex services_mutex_
Service mutex (protects services_ and sop_class_to_service_)
void check_idle_timeouts()
Check for idle timeouts on handlers.
std::map< std::string, services::scp_service * > sop_class_to_service_
Map from SOP Class UID to service (non-owning pointers)
std::function< void(const std::string &session_id, const std::string &calling_ae, const std::string &called_ae)> association_established_callback
Callback type for association established events.
~dicom_server_v2()
Destructor (stops server if running)
std::function< void(const std::string &error)> error_callback
Callback type for error events.
std::shared_ptr< dicom_association_handler > find_handler(const std::string &session_id) const
Find handler by session ID.
void remove_handler(const std::string &session_id)
Remove handler by session ID.
void on_association_closed(association_closed_callback callback)
Set callback for association closed events.
void set_access_control(std::shared_ptr< security::access_control_manager > acm)
Set the access control manager for RBAC.
error_callback on_error_cb_
Error callback.
void stop(duration timeout=std::chrono::seconds{30})
Stop the server gracefully.
size_t active_associations() const noexcept
Get number of active associations.
std::mutex handlers_mutex_
Handler mutex (protects handlers_ map)
bool is_access_control_enabled() const noexcept
Check if access control is enabled.
void on_disconnection(std::string_view session_id)
Handle disconnection notification.
Result< std::monostate > start()
Start the server.
void set_access_control_enabled(bool enabled)
Enable or disable access control enforcement.
std::atomic< bool > access_control_enabled_
Whether access control is enabled.
std::vector< std::string > supported_sop_classes() const
Get list of supported SOP Class UIDs.
std::shared_ptr< security::access_control_manager > get_access_control() const noexcept
Get the access control manager.
void on_connection(std::shared_ptr< kcenon::network::interfaces::i_session > session)
Handle new connection from server.
server_statistics stats_
Server statistics.
std::condition_variable shutdown_cv_
Shutdown condition variable.
DICOM server implementation using network_system's messaging_server.
constexpr size_t AE_TITLE_LENGTH
AE Title length (fixed 16 characters, space-padded)
Definition pdu_types.h:273
std::shared_ptr< scp_service > scp_service_ptr
Shared pointer type for SCP services.
kcenon::common::error_info error_info
Error information type.
Definition result.h:40
size_t max_associations
Maximum concurrent associations (0 = unlimited)
std::chrono::seconds idle_timeout
Idle timeout for associations (0 = no timeout)
uint16_t port
Port to listen on (default: 11112, standard alternate DICOM port)
std::string ae_title
Application Entity Title for this server (16 chars max)
Statistics for server monitoring.
std::chrono::steady_clock::time_point last_activity
Time of last activity.
uint64_t total_associations
Total associations since server start.
uint64_t messages_processed
Total DIMSE messages processed.
size_t active_associations
Currently active associations.
std::chrono::steady_clock::time_point start_time
Server start time.
uint64_t bytes_received
Total bytes received.
uint64_t rejected_associations
Total associations rejected due to limit.
std::string_view uid