1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use libpetri_core::petri_net::PetriNet;
8use libpetri_export::dot_exporter::dot_export;
9use libpetri_export::mapper::sanitize;
10
11use crate::debug_event_store::DebugEventStore;
12use crate::debug_response::{NetStructure, PlaceInfo, TransitionInfo};
13use crate::place_analysis::PlaceAnalysis;
14
15pub struct DebugSession {
17 pub session_id: String,
18 pub net_name: String,
19 pub dot_diagram: String,
20 pub places: Option<PlaceAnalysis>,
21 pub transition_names: Vec<String>,
22 pub event_store: Arc<DebugEventStore>,
23 pub start_time: u64,
24 pub active: bool,
25 pub imported_structure: Option<NetStructure>,
26}
27
28pub type SessionCompletionListener = Box<dyn Fn(&DebugSession) + Send + Sync>;
30
31pub fn build_net_structure(session: &DebugSession) -> NetStructure {
33 if let Some(ref imported) = session.imported_structure {
34 return imported.clone();
35 }
36
37 let Some(ref places) = session.places else {
38 return NetStructure {
39 places: Vec::new(),
40 transitions: Vec::new(),
41 };
42 };
43
44 let place_infos: Vec<PlaceInfo> = places
45 .data()
46 .iter()
47 .map(|(name, info)| PlaceInfo {
48 name: name.clone(),
49 graph_id: format!("p_{}", sanitize(name)),
50 token_type: info.token_type.clone(),
51 is_start: !info.has_incoming,
52 is_end: !info.has_outgoing,
53 is_environment: false,
54 })
55 .collect();
56
57 let transition_infos: Vec<TransitionInfo> = session
58 .transition_names
59 .iter()
60 .map(|name| TransitionInfo {
61 name: name.clone(),
62 graph_id: format!("t_{}", sanitize(name)),
63 })
64 .collect();
65
66 NetStructure {
67 places: place_infos,
68 transitions: transition_infos,
69 }
70}
71
72pub type EventStoreFactory = Box<dyn Fn(&str) -> DebugEventStore + Send + Sync>;
74
75pub struct DebugSessionRegistry {
77 sessions: HashMap<String, DebugSession>,
78 max_sessions: usize,
79 event_store_factory: EventStoreFactory,
80 completion_listeners: Vec<SessionCompletionListener>,
81}
82
83impl DebugSessionRegistry {
84 pub fn new() -> Self {
86 Self::with_options(50, None, Vec::new())
87 }
88
89 pub fn with_options(
91 max_sessions: usize,
92 event_store_factory: Option<EventStoreFactory>,
93 completion_listeners: Vec<SessionCompletionListener>,
94 ) -> Self {
95 Self {
96 sessions: HashMap::new(),
97 max_sessions,
98 event_store_factory: event_store_factory
99 .unwrap_or_else(|| Box::new(|id: &str| DebugEventStore::new(id.to_string()))),
100 completion_listeners,
101 }
102 }
103
104 pub fn register(&mut self, session_id: String, net: &PetriNet) -> Arc<DebugEventStore> {
106 let dot_diagram = dot_export(net, None);
107 let places = PlaceAnalysis::from_net(net);
108 let event_store = Arc::new((self.event_store_factory)(&session_id));
109
110 let transition_names: Vec<String> = net
111 .transitions()
112 .iter()
113 .map(|t| t.name().to_string())
114 .collect();
115
116 let session = DebugSession {
117 session_id: session_id.clone(),
118 net_name: net.name().to_string(),
119 dot_diagram,
120 places: Some(places),
121 transition_names,
122 event_store: Arc::clone(&event_store),
123 start_time: now_ms(),
124 active: true,
125 imported_structure: None,
126 };
127
128 self.evict_if_necessary();
129 self.sessions.insert(session_id, session);
130 event_store
131 }
132
133 pub fn complete(&mut self, session_id: &str) {
135 if let Some(session) = self.sessions.get_mut(session_id) {
136 session.active = false;
137 for listener in &self.completion_listeners {
138 listener(session);
139 }
140 }
141 }
142
143 pub fn remove(&mut self, session_id: &str) -> Option<DebugSession> {
145 let removed = self.sessions.remove(session_id);
146 if let Some(ref session) = removed {
147 session.event_store.close();
148 }
149 removed
150 }
151
152 pub fn get_session(&self, session_id: &str) -> Option<&DebugSession> {
154 self.sessions.get(session_id)
155 }
156
157 pub fn list_sessions(&self, limit: usize) -> Vec<&DebugSession> {
159 let mut sessions: Vec<&DebugSession> = self.sessions.values().collect();
160 sessions.sort_by(|a, b| b.start_time.cmp(&a.start_time));
161 sessions.truncate(limit);
162 sessions
163 }
164
165 pub fn list_active_sessions(&self, limit: usize) -> Vec<&DebugSession> {
167 let mut sessions: Vec<&DebugSession> =
168 self.sessions.values().filter(|s| s.active).collect();
169 sessions.sort_by(|a, b| b.start_time.cmp(&a.start_time));
170 sessions.truncate(limit);
171 sessions
172 }
173
174 pub fn size(&self) -> usize {
176 self.sessions.len()
177 }
178
179 pub fn register_imported(
181 &mut self,
182 session_id: String,
183 net_name: String,
184 dot_diagram: String,
185 structure: NetStructure,
186 event_store: Arc<DebugEventStore>,
187 start_time: u64,
188 ) {
189 self.evict_if_necessary();
190
191 let session = DebugSession {
192 session_id: session_id.clone(),
193 net_name,
194 dot_diagram,
195 places: None,
196 transition_names: Vec::new(),
197 event_store,
198 start_time,
199 active: false,
200 imported_structure: Some(structure),
201 };
202
203 self.sessions.insert(session_id, session);
204 }
205
206 fn evict_if_necessary(&mut self) {
207 if self.sessions.len() < self.max_sessions {
208 return;
209 }
210
211 let mut candidates: Vec<(&String, bool, u64)> = self
213 .sessions
214 .iter()
215 .map(|(id, s)| (id, s.active, s.start_time))
216 .collect();
217 candidates.sort_by(|a, b| {
218 if a.1 != b.1 {
219 return if a.1 {
220 std::cmp::Ordering::Greater
221 } else {
222 std::cmp::Ordering::Less
223 };
224 }
225 a.2.cmp(&b.2)
226 });
227
228 let to_remove: Vec<String> = candidates
229 .iter()
230 .take_while(|_| self.sessions.len() >= self.max_sessions)
231 .map(|(id, _, _)| (*id).clone())
232 .collect();
233
234 for id in to_remove {
235 if self.sessions.len() < self.max_sessions {
236 break;
237 }
238 if let Some(session) = self.sessions.remove(&id) {
239 session.event_store.close();
240 }
241 }
242 }
243}
244
245impl Default for DebugSessionRegistry {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251fn now_ms() -> u64 {
252 SystemTime::now()
253 .duration_since(UNIX_EPOCH)
254 .unwrap_or_default()
255 .as_millis() as u64
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use libpetri_core::input::one;
262 use libpetri_core::output::out_place;
263 use libpetri_core::place::Place;
264 use libpetri_core::transition::Transition;
265
266 fn test_net() -> PetriNet {
267 let p1 = Place::<i32>::new("p1");
268 let p2 = Place::<i32>::new("p2");
269 let t = Transition::builder("t1")
270 .input(one(&p1))
271 .output(out_place(&p2))
272 .build();
273 PetriNet::builder("test").transition(t).build()
274 }
275
276 #[test]
277 fn register_and_get_session() {
278 let mut registry = DebugSessionRegistry::new();
279 let net = test_net();
280 let _store = registry.register("s1".into(), &net);
281
282 let session = registry.get_session("s1").unwrap();
283 assert_eq!(session.net_name, "test");
284 assert!(session.active);
285 assert!(!session.dot_diagram.is_empty());
286 }
287
288 #[test]
289 fn complete_session() {
290 let mut registry = DebugSessionRegistry::new();
291 let net = test_net();
292 let _store = registry.register("s1".into(), &net);
293
294 registry.complete("s1");
295 let session = registry.get_session("s1").unwrap();
296 assert!(!session.active);
297 }
298
299 #[test]
300 fn list_sessions() {
301 let mut registry = DebugSessionRegistry::new();
302 let net = test_net();
303 let _s1 = registry.register("s1".into(), &net);
304 let _s2 = registry.register("s2".into(), &net);
305
306 assert_eq!(registry.list_sessions(10).len(), 2);
307 assert_eq!(registry.size(), 2);
308 }
309
310 #[test]
311 fn list_active_sessions() {
312 let mut registry = DebugSessionRegistry::new();
313 let net = test_net();
314 let _s1 = registry.register("s1".into(), &net);
315 let _s2 = registry.register("s2".into(), &net);
316 registry.complete("s1");
317
318 assert_eq!(registry.list_active_sessions(10).len(), 1);
319 }
320
321 #[test]
322 fn remove_session() {
323 let mut registry = DebugSessionRegistry::new();
324 let net = test_net();
325 let _store = registry.register("s1".into(), &net);
326
327 let removed = registry.remove("s1");
328 assert!(removed.is_some());
329 assert!(registry.get_session("s1").is_none());
330 assert_eq!(registry.size(), 0);
331 }
332
333 #[test]
334 fn build_net_structure_from_live_session() {
335 let mut registry = DebugSessionRegistry::new();
336 let net = test_net();
337 let _store = registry.register("s1".into(), &net);
338
339 let session = registry.get_session("s1").unwrap();
340 let structure = build_net_structure(session);
341
342 assert_eq!(structure.places.len(), 2);
343 assert_eq!(structure.transitions.len(), 1);
344
345 let p1 = structure.places.iter().find(|p| p.name == "p1").unwrap();
346 assert_eq!(p1.graph_id, "p_p1");
347 assert!(p1.is_start);
348 assert!(!p1.is_end);
349
350 let p2 = structure.places.iter().find(|p| p.name == "p2").unwrap();
351 assert!(p2.is_end);
352 assert!(!p2.is_start);
353
354 assert_eq!(structure.transitions[0].name, "t1");
355 assert_eq!(structure.transitions[0].graph_id, "t_t1");
356 }
357
358 #[test]
359 fn eviction_at_capacity() {
360 let mut registry = DebugSessionRegistry::with_options(2, None, Vec::new());
361 let net = test_net();
362
363 let _s1 = registry.register("s1".into(), &net);
364 let _s2 = registry.register("s2".into(), &net);
365 registry.complete("s1");
366 let _s3 = registry.register("s3".into(), &net);
368
369 assert_eq!(registry.size(), 2);
370 assert!(registry.get_session("s1").is_none());
371 assert!(registry.get_session("s2").is_some());
372 assert!(registry.get_session("s3").is_some());
373 }
374}