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>
26#endif
27
28#if KCENON_HAS_COMMON_SYSTEM
29using kcenon::common::error_info;
30#endif
31
33
34// =============================================================================
35// Construction / Destruction
36// =============================================================================
37
39 : config_(config) {
40 stats_.start_time = clock::now();
42}
43
45 try {
46 if (running_) {
47 stop();
48 }
49 } catch (...) {
50 // Suppress exceptions in destructor to prevent std::terminate
51 }
52}
53
54// =============================================================================
55// Service Registration
56// =============================================================================
57
59 if (!service) {
60 return;
61 }
62
63 std::lock_guard<std::mutex> lock(services_mutex_);
64
65 // Register SOP Class mappings
66 for (const auto& sop_class : service->supported_sop_classes()) {
67 sop_class_to_service_[sop_class] = service.get();
68 }
69
70 services_.push_back(std::move(service));
71}
72
73std::vector<std::string> dicom_server_v2::supported_sop_classes() const {
74 std::lock_guard<std::mutex> lock(services_mutex_);
75
76 std::vector<std::string> sop_classes;
77 sop_classes.reserve(sop_class_to_service_.size());
78
79 for (const auto& [uid, _] : sop_class_to_service_) {
80 sop_classes.push_back(uid);
81 }
82
83 return sop_classes;
84}
85
86// =============================================================================
87// Lifecycle Management
88// =============================================================================
89
91 if (running_.exchange(true)) {
92 return error_info("Server already running");
93 }
94
95 // Validate configuration
96 if (config_.ae_title.empty()) {
97 running_ = false;
98 return error_info("AE Title cannot be empty");
99 }
100
101 if (config_.ae_title.length() > AE_TITLE_LENGTH) {
102 running_ = false;
103 return error_info("AE Title exceeds 16 characters");
104 }
105
106 if (config_.port == 0) {
107 running_ = false;
108 return error_info("Invalid port number");
109 }
110
111 // Check that at least one service is registered
112 {
113 std::lock_guard<std::mutex> lock(services_mutex_);
114 if (services_.empty()) {
115 running_ = false;
116 return error_info("No services registered");
117 }
118 }
119
120 // Reset statistics
121 {
122 std::lock_guard<std::mutex> lock(stats_mutex_);
123 stats_.start_time = clock::now();
130 stats_.bytes_sent = 0;
131 }
132
133#ifdef PACS_WITH_NETWORK_SYSTEM
134 try {
135 // Create TCP server via tcp_facade
136 kcenon::network::facade::tcp_facade facade;
137 kcenon::network::facade::tcp_facade::server_config srv_cfg;
138 srv_cfg.server_id = config_.ae_title;
139 srv_cfg.port = config_.port;
140 auto server_result = compat::factory_result(facade.create_server(srv_cfg));
141 if (server_result.is_err()) {
142 running_ = false;
143 return server_result.error();
144 }
145 server_ = std::move(server_result.value());
146
147 // Set up server-level callbacks
148 server_->set_connection_callback(
149 [this](std::shared_ptr<kcenon::network::interfaces::i_session> session) {
150 on_connection(std::move(session));
151 });
152
153 server_->set_disconnection_callback(
154 [this](std::string_view session_id) {
155 on_disconnection(session_id);
156 });
157
158 server_->set_receive_callback(
159 [this](std::string_view session_id,
160 const std::vector<uint8_t>& data) {
161 on_receive(session_id, data);
162 });
163
164 server_->set_error_callback(
165 [this](std::string_view session_id,
166 std::error_code ec) {
167 on_network_error(session_id, ec);
168 });
169
170 // Start the server on configured port
171 auto result = server_->start(config_.port);
172 if (result.is_err()) {
173 running_ = false;
174 server_.reset();
175 return error_info("Failed to start server");
176 }
177
178 return std::monostate{};
179 } catch (const std::exception& e) {
180 running_ = false;
181 if (server_) {
182 server_.reset();
183 }
184 return error_info(std::string("Exception during server start: ") + e.what());
185 } catch (...) {
186 running_ = false;
187 if (server_) {
188 server_.reset();
189 }
190 return error_info("Unknown exception during server start");
191 }
192#else
193 running_ = false;
194 return error_info("dicom_server_v2 requires PACS_WITH_NETWORK_SYSTEM");
195#endif
196}
197
199 if (!running_.exchange(false)) {
200 return; // Already stopped
201 }
202
203#ifdef PACS_WITH_NETWORK_SYSTEM
204 // Phase 1: Stop accepting new connections
205 if (server_) {
206 try {
207 // Stop the server - this closes the acceptor
208 (void)server_->stop();
209 } catch (...) {
210 // Suppress exceptions during server stop
211 }
212 }
213
214 // Phase 2: Wait for handlers to complete gracefully
215 auto deadline = clock::now() + timeout;
216 {
217 std::unique_lock<std::mutex> lock(handlers_mutex_);
218 while (!handlers_.empty() && clock::now() < deadline) {
219 // Release lock while waiting
220 lock.unlock();
221 std::this_thread::sleep_for(std::chrono::milliseconds{100});
222 lock.lock();
223 }
224 }
225
226 // Phase 3: Force stop remaining handlers
227 // Collect handlers first, then stop them without holding the lock
228 // to avoid potential deadlock if handler->stop() triggers callbacks
229 std::vector<std::shared_ptr<dicom_association_handler>> handlers_to_stop;
230 {
231 std::lock_guard<std::mutex> lock(handlers_mutex_);
232 handlers_to_stop.reserve(handlers_.size());
233 for (auto& [session_id, handler] : handlers_) {
234 handlers_to_stop.push_back(handler);
235 }
236 handlers_.clear();
237 }
238
239 // Stop handlers without holding the lock
240 for (auto& handler : handlers_to_stop) {
241 try {
242 handler->stop(false); // Force abort
243 } catch (...) {
244 // Suppress exceptions during handler stop
245 }
246 }
247 handlers_to_stop.clear();
248
249 // Allow any pending callbacks to complete
250 std::this_thread::sleep_for(std::chrono::milliseconds{50});
251
252 // Clear callbacks to break reference cycles before server is destroyed.
253 // The server object itself is kept alive until dicom_server_v2 is destroyed
254 // to avoid use-after-free in the adapter's background I/O threads.
255 if (server_) {
256 server_->set_connection_callback(nullptr);
257 server_->set_disconnection_callback(nullptr);
258 server_->set_receive_callback(nullptr);
259 server_->set_error_callback(nullptr);
260 }
261#else
262 (void)timeout; // Unused without network_system
263#endif
264
265 // Notify shutdown waiters
266 {
267 std::lock_guard<std::mutex> lock(shutdown_mutex_);
268 shutdown_cv_.notify_all();
269 }
270}
271
273 std::unique_lock<std::mutex> lock(shutdown_mutex_);
274 shutdown_cv_.wait(lock, [this]() { return !running_; });
275}
276
277// =============================================================================
278// Status Queries
279// =============================================================================
280
281bool dicom_server_v2::is_running() const noexcept {
282 return running_;
283}
284
286 std::lock_guard<std::mutex> lock(handlers_mutex_);
287 return handlers_.size();
288}
289
291 std::lock_guard<std::mutex> lock(stats_mutex_);
292 server_statistics result = stats_;
294 return result;
295}
296
297const server_config& dicom_server_v2::config() const noexcept {
298 return config_;
299}
300
301// =============================================================================
302// Callbacks
303// =============================================================================
304
306 std::lock_guard<std::mutex> lock(callback_mutex_);
307 on_established_cb_ = std::move(callback);
308}
309
311 std::lock_guard<std::mutex> lock(callback_mutex_);
312 on_closed_cb_ = std::move(callback);
313}
314
316 std::lock_guard<std::mutex> lock(callback_mutex_);
317 on_error_cb_ = std::move(callback);
318}
319
320// =============================================================================
321// Network System Callbacks
322// =============================================================================
323
325 std::shared_ptr<kcenon::network::interfaces::i_session> session) {
326
327 if (!running_ || !session) {
328 return;
329 }
330
331 // Check max associations limit
332 {
333 std::lock_guard<std::mutex> lock(handlers_mutex_);
334 if (config_.max_associations > 0 &&
336 // Reject due to resource limit
337 report_error("Max associations limit reached, rejecting connection");
338
339 std::lock_guard<std::mutex> stats_lock(stats_mutex_);
341
342#ifdef PACS_WITH_NETWORK_SYSTEM
343 session->close();
344#endif
345 return;
346 }
347 }
348
349 // Create handler for this session
350 create_handler(std::move(session));
351}
352
353void dicom_server_v2::on_disconnection(std::string_view session_id) {
354 // Skip if server is shutting down
355 if (!running_) {
356 return;
357 }
358
359 std::string sid(session_id);
360
361 // Notify handler of disconnection before removing it
362 auto handler = find_handler(sid);
363 if (handler) {
364 handler->handle_disconnect();
365 }
366
367 // Remove handler for this session
368 remove_handler(sid);
369}
370
372 std::string_view session_id,
373 const std::vector<uint8_t>& data) {
374
375 std::string sid(session_id);
376
377 // Find handler and forward data
378 auto handler = find_handler(sid);
379 if (handler) {
380 // Forward data to handler for PDU processing
381 handler->feed_data(data);
382
383 // Update statistics
384 {
385 std::lock_guard<std::mutex> lock(stats_mutex_);
386 stats_.bytes_received += data.size();
387 stats_.last_activity = clock::now();
388 }
389 }
390}
391
393 std::string_view session_id,
394 std::error_code ec) {
395
396 // Skip if server is shutting down
397 if (!running_) {
398 return;
399 }
400
401 std::string sid(session_id);
402
403 std::ostringstream oss;
404 oss << "Network error on session " << sid
405 << ": " << ec.message() << " (" << ec.value() << ")";
406 report_error(oss.str());
407
408 // Notify handler of the error
409 auto handler = find_handler(sid);
410 if (handler) {
411 handler->handle_error(ec);
412 }
413
414 // Remove the handler - it will clean itself up
415 remove_handler(sid);
416}
417
418// =============================================================================
419// Handler Management
420// =============================================================================
421
423 std::shared_ptr<kcenon::network::interfaces::i_session> session) {
424
425#ifdef PACS_WITH_NETWORK_SYSTEM
426 const std::string session_id(session->id());
427#else
428 const std::string session_id;
429 (void)session;
430#endif
431
432 // Build service map for handler
433 auto service_map = build_service_map();
434
435 // Create handler
436 auto handler = std::make_shared<dicom_association_handler>(
437 std::move(session), config_, service_map);
438
439 // Set up access control if configured
440 {
441 std::lock_guard<std::mutex> acl_lock(acl_mutex_);
442 if (access_control_) {
443 handler->set_access_control(access_control_);
444 handler->set_access_control_enabled(access_control_enabled_);
445 }
446 }
447
448 // Set up handler callbacks
449 auto weak_this = std::weak_ptr<dicom_server_v2*>(
450 std::shared_ptr<dicom_server_v2*>(nullptr, [](dicom_server_v2**) {}));
451 // Note: We can't use shared_from_this() since dicom_server_v2 doesn't inherit
452 // from enable_shared_from_this. Instead, capture 'this' directly since the
453 // handler's lifetime is bounded by the server's lifetime.
454
455 handler->set_established_callback(
456 [this](const std::string& sid, const std::string& calling_ae,
457 const std::string& called_ae) {
458 // Update statistics
459 {
460 std::lock_guard<std::mutex> lock(stats_mutex_);
462 stats_.last_activity = clock::now();
463 }
464
465 // Forward to user callback
466 {
467 std::lock_guard<std::mutex> lock(callback_mutex_);
468 if (on_established_cb_) {
469 on_established_cb_(sid, calling_ae, called_ae);
470 }
471 }
472 });
473
474 handler->set_closed_callback(
475 [this](const std::string& sid, bool graceful) {
476 // Forward to user callback
477 {
478 std::lock_guard<std::mutex> lock(callback_mutex_);
479 if (on_closed_cb_) {
480 on_closed_cb_(sid, graceful);
481 }
482 }
483
484 // Remove handler from map
485 remove_handler(sid);
486 });
487
488 handler->set_error_callback(
489 [this](const std::string& /*sid*/, const std::string& error) {
490 report_error(error);
491 });
492
493 // Register handler
494 {
495 std::lock_guard<std::mutex> lock(handlers_mutex_);
496 handlers_[session_id] = handler;
497 }
498
499 // Start handler (begins processing PDUs)
500 handler->start();
501}
502
503void dicom_server_v2::remove_handler(const std::string& session_id) {
504 // Skip if server is shutting down - handlers are cleaned up in stop()
505 if (!running_) {
506 return;
507 }
508
509 std::lock_guard<std::mutex> lock(handlers_mutex_);
510 auto it = handlers_.find(session_id);
511 if (it != handlers_.end()) {
512 // Handler cleans itself up via stop() in destructor
513 handlers_.erase(it);
514 }
515}
516
517std::shared_ptr<dicom_association_handler>
518dicom_server_v2::find_handler(const std::string& session_id) const {
519 std::lock_guard<std::mutex> lock(handlers_mutex_);
520 auto it = handlers_.find(session_id);
521 if (it != handlers_.end()) {
522 return it->second;
523 }
524 return nullptr;
525}
526
528 if (config_.idle_timeout.count() == 0) {
529 return; // No timeout configured
530 }
531
532 auto now = clock::now();
533 std::vector<std::string> timed_out;
534
535 {
536 std::lock_guard<std::mutex> lock(handlers_mutex_);
537 for (const auto& [session_id, handler] : handlers_) {
538 auto idle_duration = std::chrono::duration_cast<std::chrono::seconds>(
539 now - handler->last_activity());
540
541 if (idle_duration >= config_.idle_timeout) {
542 timed_out.push_back(session_id);
543 }
544 }
545 }
546
547 // Stop timed-out handlers
548 for (const auto& session_id : timed_out) {
549 auto handler = find_handler(session_id);
550 if (handler) {
551 handler->stop(false); // Force abort
552 }
553 }
554}
555
556// =============================================================================
557// Internal Helpers
558// =============================================================================
559
564
565void dicom_server_v2::report_error(const std::string& error) {
566 std::lock_guard<std::mutex> lock(callback_mutex_);
567 if (on_error_cb_) {
568 on_error_cb_(error);
569 }
570}
571
572// =============================================================================
573// Security / Access Control
574// =============================================================================
575
577 std::shared_ptr<security::access_control_manager> acm) {
578 std::lock_guard<std::mutex> lock(acl_mutex_);
579 access_control_ = std::move(acm);
580 if (access_control_) {
582 }
583}
584
585std::shared_ptr<security::access_control_manager>
587 std::lock_guard<std::mutex> lock(acl_mutex_);
588 return access_control_;
589}
590
594
598
599} // 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.
auto factory_result(kcenon::common::Result< T > result) -> kcenon::common::Result< T >
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
Configuration for DICOM server.
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