1use tokio::sync::mpsc::{error::SendError, unbounded_channel, UnboundedReceiver, UnboundedSender};
51use tokio::time::{Duration, Instant};
52
53use std::collections::{BTreeMap, BTreeSet, HashMap};
54use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}};
55
56use rpfm_ipc::api::{ApiError, Done, Request, RpcOutcome, RpcRequest, RpcResponse};
57use rpfm_ipc::api::diagnostics::RunDiagnostics;
58use rpfm_ipc::api::jobs::JobStarted;
59use rpfm_ipc::api::session::Configure;
60use rpfm_ipc::helpers::SessionInfo;
61use rpfm_ipc::settings::Settings;
62use rpfm_ipc::settings_keys::AUTOSAVE_INTERVAL;
63use rpfm_telemetry::{error, info};
64
65use crate::api;
66use crate::background_thread;
67use crate::apply_telemetry_settings;
68use crate::jobs::{self, JobRegistry};
69
70pub const SESSION_SENDER_ERROR: &str = "Error in session communication system. Sender failed to send message.";
72
73pub const DEFAULT_SESSION_TIMEOUT_SECS: u64 = 300;
75
76const CLEANUP_INTERVAL: Duration = Duration::from_secs(10);
78
79pub type SessionId = u64;
85
86#[derive(Debug)]
88pub enum SessionMessage {
89
90 Api(RpcRequest, UnboundedSender<RpcResponse>),
92
93 Job(u64, RpcRequest),
95
96 Autosave,
98
99 Exit,
101}
102
103pub struct SessionManager {
108
109 sessions: Mutex<HashMap<SessionId, ManagedSession>>,
111
112 next_id: Mutex<SessionId>,
114
115 timeout: Duration,
117}
118
119struct ManagedSession {
121
122 session: Arc<Session>,
124
125 disconnected_at: Option<Instant>,
127}
128
129pub struct Session {
134
135 id: SessionId,
137
138 is_mcp: bool,
140
141 last_activity: Mutex<Instant>,
146
147 sender: UnboundedSender<SessionMessage>,
149
150 connection_count: AtomicU32,
152
153 shutdown_requested: AtomicBool,
155
156 pack_names: RwLock<BTreeMap<String, String>>,
158
159 jobs: Arc<JobRegistry>,
161
162 settings: RwLock<Arc<Settings>>,
164
165 queued: AtomicUsize,
167
168 diagnostics_check: Mutex<Option<DiagnosticsCheck>>,
170}
171
172#[derive(Debug)]
174struct DiagnosticsCheck {
175
176 job: u64,
178
179 paths: Vec<String>,
181}
182
183impl Session {
188
189 pub fn new(id: SessionId, is_mcp: bool) -> Arc<Self> {
194 let (sender, receiver) = unbounded_channel();
195
196 let session = Arc::new(Self {
197 id,
198 is_mcp,
199 last_activity: Mutex::new(Instant::now()),
200 sender,
201 connection_count: AtomicU32::new(0),
202 shutdown_requested: AtomicBool::new(false),
203 pack_names: RwLock::new(BTreeMap::new()),
204 jobs: Arc::new(JobRegistry::default()),
205 settings: RwLock::new(Arc::new(Settings::init(false))),
206 queued: AtomicUsize::new(0),
207 diagnostics_check: Mutex::new(None),
208 });
209
210 let session_clone = session.clone();
212 let spawned = std::thread::Builder::new().name(format!("session-{id}")).spawn(move || {
213 info!("Session {} background thread starting...", id);
214 background_thread::background_loop(receiver, session_clone);
215 info!("Session {} background thread terminated.", id);
216 });
217
218 if let Err(error) = spawned {
219 error!("Session {}: failed to start its background thread: {}", id, error);
220 }
221
222 Self::start_autosave_timer(&session);
223 session
224 }
225
226 fn start_autosave_timer(session: &Arc<Self>) {
230
231 let session = Arc::downgrade(session);
233 tokio::spawn(async move {
234 while let Some(minutes) = session.upgrade().map(|session| session.settings().i32(AUTOSAVE_INTERVAL)) {
235
236 tokio::time::sleep(Duration::from_secs(60 * minutes.max(1) as u64)).await;
238
239 let Some(alive_session) = session.upgrade().filter(|session| !session.is_shutdown_requested()) else {
240 break;
241 };
242
243 if minutes > 0 {
244 let _ = alive_session.enqueue(SessionMessage::Autosave);
245 }
246 }
247 });
248 }
249
250 pub fn id(&self) -> SessionId {
252 self.id
253 }
254
255 pub fn is_mcp(&self) -> bool {
257 self.is_mcp
258 }
259
260 fn touch(&self) {
262 *self.last_activity.lock().unwrap() = Instant::now();
263 }
264
265 pub fn last_activity(&self) -> Instant {
267 *self.last_activity.lock().unwrap()
268 }
269
270 pub fn connect(&self) {
272 self.connection_count.fetch_add(1, Ordering::SeqCst);
273 }
274
275 pub fn disconnect(&self) {
277 self.connection_count.fetch_sub(1, Ordering::SeqCst);
278 }
279
280 pub fn connection_count(&self) -> u32 {
282 self.connection_count.load(Ordering::SeqCst)
283 }
284
285 pub fn is_shutdown_requested(&self) -> bool {
287 self.shutdown_requested.load(Ordering::SeqCst)
288 }
289
290 pub fn pack_names(&self) -> Vec<String> {
292 self.pack_names.read().unwrap().values().cloned().collect()
293 }
294
295 pub fn set_pack_name(&self, pack_key: &str, name: &str) {
297 self.pack_names.write().unwrap().insert(pack_key.to_owned(), name.to_owned());
298 }
299
300 pub fn remove_pack(&self, pack_key: &str) {
302 self.pack_names.write().unwrap().remove(pack_key);
303 }
304
305 pub fn shutdown(&self) {
307 info!("Session {} shutting down...", self.id);
308
309 if self.shutdown_requested.swap(true, Ordering::SeqCst) {
310 info!("Session {} already marked for shutdown before...", self.id);
311 return;
312 }
313
314 let _ = self.sender.send(SessionMessage::Exit);
316 }
317
318 pub fn jobs(&self) -> &Arc<JobRegistry> {
320 &self.jobs
321 }
322
323 pub fn settings(&self) -> Arc<Settings> {
325 self.settings.read().unwrap().clone()
326 }
327
328 fn configure(&self, request: RpcRequest) -> RpcResponse {
330 let result = api::parse_params::<Configure>(request.params).and_then(|configure| {
331 apply_telemetry_settings(&configure.settings);
332 *self.settings.write().unwrap() = Arc::new(configure.settings);
333 serde_json::to_value(Done {}).map_err(|error| ApiError::Internal(error.to_string()))
334 });
335
336 RpcResponse::new(request.id, result)
337 }
338
339 pub fn call(&self, request: RpcRequest) -> UnboundedReceiver<RpcResponse> {
346 self.touch();
347 let (sender_back, receiver_back) = unbounded_channel();
348
349 if request.method == Configure::METHOD {
350 let _ = sender_back.send(self.configure(request));
351 return receiver_back;
352 }
353
354 if jobs::is_job_control_method(&request.method) {
355 jobs::handle_request(self.jobs.clone(), request, sender_back);
356 return receiver_back;
357 }
358
359 if api::is_stateless_method(&request.method) {
360 let settings = self.settings();
361 tokio::spawn(async move {
362 let id = request.id;
363 let response = tokio::task::spawn_blocking(move || api::dispatch_stateless(request, &settings)).await
364 .unwrap_or_else(|error| RpcResponse::new(id, Err(ApiError::Internal(format!("The background task failed: {error}")))));
365
366 let _ = sender_back.send(response);
367 });
368 return receiver_back;
369 }
370
371 if api::is_job_method(&request.method) {
372 let job = self.jobs.create(&request.method);
373 let id = request.id;
374 let request = if request.method == RunDiagnostics::METHOD {
375 self.replace_diagnostics_check(job, request)
376 } else {
377 request
378 };
379
380 if let Err(error) = self.enqueue(SessionMessage::Job(job, request)) {
381 let message = format!("{SESSION_SENDER_ERROR}: {error}");
382 info!("{message}");
383 self.jobs.finish(job, RpcOutcome::Error(ApiError::Internal(message).into()));
384 }
385
386 let started = serde_json::to_value(JobStarted { job }).map_err(|error| ApiError::Internal(error.to_string()));
387 let _ = sender_back.send(RpcResponse::new(id, started));
388 return receiver_back;
389 }
390
391 if let Err(error) = self.enqueue(SessionMessage::Api(request, sender_back)) {
392 let message = format!("{SESSION_SENDER_ERROR}: {error}");
393 info!("{message}");
394 if let SessionMessage::Api(request, sender_back) = error.0 {
395 let _ = sender_back.send(RpcResponse::new(request.id, Err(ApiError::Internal(message))));
396 }
397 }
398 receiver_back
399 }
400
401 fn enqueue(&self, message: SessionMessage) -> Result<(), SendError<SessionMessage>> {
403 self.queued.fetch_add(1, Ordering::SeqCst);
404 self.sender.send(message).inspect_err(|_| { self.queued.fetch_sub(1, Ordering::SeqCst); })
405 }
406
407 pub fn dequeued(&self) {
409 self.queued.fetch_sub(1, Ordering::SeqCst);
410 }
411
412 pub fn has_queued_requests(&self) -> bool {
414 self.queued.load(Ordering::SeqCst) > 0
415 }
416
417 fn replace_diagnostics_check(&self, job: u64, mut request: RpcRequest) -> RpcRequest {
431 let Ok(mut params) = api::parse_params::<RunDiagnostics>(request.params.clone()) else {
432 return request;
433 };
434
435 let mut last_check = self.diagnostics_check.lock().unwrap();
436 let pending = last_check.take().filter(|check| self.jobs.status(check.job).is_some_and(|status| !status.state.has_ended()));
437 if let Some(pending) = pending {
438
439 params.paths = if pending.paths.is_empty() || params.paths.is_empty() {
441 vec![]
442 } else {
443 pending.paths.into_iter().chain(params.paths).collect::<BTreeSet<_>>().into_iter().collect()
444 };
445
446 let _ = self.jobs.cancel(pending.job);
447 if let Ok(params) = serde_json::to_value(¶ms) {
448 request.params = params;
449 }
450 }
451
452 *last_check = Some(DiagnosticsCheck { job, paths: params.paths });
453 request
454 }
455
456 pub fn requeue_diagnostics_check(&self, job: u64, request: RpcRequest) {
463 let last_check = self.diagnostics_check.lock().unwrap();
464 if last_check.as_ref().is_some_and(|check| check.job == job) && self.jobs.requeue(job) {
465 if self.enqueue(SessionMessage::Job(job, request)).is_err() {
466 self.jobs.finish_cancelled(job);
467 }
468 } else {
469 self.jobs.finish_cancelled(job);
470 }
471 }
472}
473
474impl Default for SessionManager {
475 fn default() -> Self {
476 Self {
477 sessions: Mutex::new(HashMap::new()),
478 next_id: Mutex::new(1),
479 timeout: Duration::from_secs(DEFAULT_SESSION_TIMEOUT_SECS),
480 }
481 }
482}
483
484impl SessionManager {
485
486 pub fn create_session(&self) -> Arc<Session> {
488 self.create_session_internal(false)
489 }
490
491 pub fn mcp_session(&self) -> Arc<Session> {
497 let mut sessions = self.sessions.lock().unwrap();
498 let existing = sessions.values()
499 .find(|managed| managed.session.is_mcp() && !managed.session.is_shutdown_requested())
500 .map(|managed| managed.session.clone());
501
502 let session = existing.unwrap_or_else(|| self.insert_session(&mut sessions, true));
503
504 session.touch();
506 session
507 }
508
509 fn create_session_internal(&self, is_mcp: bool) -> Arc<Session> {
510 let mut sessions = self.sessions.lock().unwrap();
511 self.insert_session(&mut sessions, is_mcp)
512 }
513
514 fn insert_session(&self, sessions: &mut HashMap<SessionId, ManagedSession>, is_mcp: bool) -> Arc<Session> {
516 let id = {
517 let mut next_id = self.next_id.lock().unwrap();
518 let id = *next_id;
519 *next_id += 1;
520 id
521 };
522
523 let session = Session::new(id, is_mcp);
524
525 if !is_mcp {
528 session.connect();
529 }
530
531 sessions.insert(id, ManagedSession {
532 session: session.clone(),
533 disconnected_at: None,
534 });
535
536 info!("Created new {} session with ID: {}", if is_mcp { "MCP" } else { "WebSocket" }, id);
537 session
538 }
539
540 pub fn get_or_create_session(&self, session_id: Option<SessionId>) -> (Arc<Session>, bool) {
547 if let Some(id) = session_id {
548 let mut sessions = self.sessions.lock().unwrap();
549 if let Some(managed) = sessions.get_mut(&id) {
550
551 if !managed.session.is_shutdown_requested() {
553 managed.session.connect();
554 managed.disconnected_at = None;
555 info!("Client reconnected to existing session {}", id);
556 return (managed.session.clone(), false);
557 }
558 }
559 }
560
561 (self.create_session(), true)
564 }
565
566 pub fn get_session(&self, id: SessionId) -> Option<Arc<Session>> {
568 let sessions = self.sessions.lock().unwrap();
569 sessions.get(&id).map(|m| m.session.clone())
570 }
571
572 pub fn client_disconnected(&self, id: SessionId) {
577 let mut sessions = self.sessions.lock().unwrap();
578 if let Some(managed) = sessions.get_mut(&id) {
579 managed.session.disconnect();
580
581 if managed.session.connection_count() == 0 {
582 managed.disconnected_at = Some(Instant::now());
583 info!("Session {} has no active connections, will timeout in {:?}", id, self.timeout);
584 }
585 }
586 }
587
588 pub fn cleanup_expired_sessions(&self) -> bool {
596 let now = Instant::now();
597 let mut websocket_session_removed = false;
598
599 let mut sessions = self.sessions.lock().unwrap();
601 sessions.retain(|id, managed| {
602 let expired = if managed.session.is_mcp() {
603
604 now.duration_since(managed.session.last_activity()) >= self.timeout
607 && managed.session.pack_names().is_empty()
608 && !managed.session.jobs().has_unfinished_jobs()
609 } else {
610 managed.disconnected_at.is_some_and(|disconnected_at| now.duration_since(disconnected_at) >= self.timeout)
611 && managed.session.connection_count() == 0
612 };
613
614 if expired {
615 info!("Removing session {}", id);
616 managed.session.shutdown();
617 websocket_session_removed |= !managed.session.is_mcp();
618 }
619
620 !expired
621 });
622
623 websocket_session_removed
624 }
625
626 pub fn exit_if_no_sessions(&self) {
631 if self.session_count() == 0 {
632 info!("No more active sessions, shutting down server...");
633 rpfm_telemetry::flush("Server Action Telemetry");
634 std::process::exit(0);
635 }
636 }
637
638 pub fn remove_session(&self, id: SessionId) -> Option<Arc<Session>> {
640 let mut sessions = self.sessions.lock().unwrap();
641 if let Some(managed) = sessions.remove(&id) {
642 info!("Removing session {}", id);
643 managed.session.shutdown();
644 Some(managed.session)
645 } else {
646 None
647 }
648 }
649
650 pub fn session_count(&self) -> usize {
652 let sessions = self.sessions.lock().unwrap();
653 sessions.len()
654 }
655
656 pub fn session_ids(&self) -> Vec<SessionId> {
658 let sessions = self.sessions.lock().unwrap();
659 sessions.keys().cloned().collect()
660 }
661
662 pub fn get_sessions_info(&self) -> Vec<SessionInfo> {
667 let sessions = self.sessions.lock().unwrap();
668 let now = Instant::now();
669
670 sessions.values().map(|managed| {
671 let timeout_remaining_secs = managed.disconnected_at.map(|disconnected_at| {
672 let elapsed = now.duration_since(disconnected_at);
673 if elapsed < self.timeout {
674 (self.timeout - elapsed).as_secs()
675 } else {
676 0
677 }
678 });
679
680 SessionInfo::new(
681 managed.session.id(),
682 managed.session.connection_count(),
683 timeout_remaining_secs,
684 managed.session.is_shutdown_requested(),
685 managed.session.pack_names(),
686 )
687 }).collect()
688 }
689
690 pub fn start_cleanup_task(manager: Arc<Self>) {
692 tokio::spawn(async move {
693 loop {
694 tokio::time::sleep(CLEANUP_INTERVAL).await;
695 if manager.cleanup_expired_sessions() {
696 manager.exit_if_no_sessions();
697 }
698 }
699 });
700 }
701}
702
703#[cfg(test)]
704#[path = "session_test.rs"]
705mod session_test;