1use crate::execution_control::collect_process_ids;
9use crate::execution_store::{ExecutionRecord, ExecutionStatus, ExecutionStore};
10use crate::output_blocks::{escape_for_links_notation, format_value_for_links_notation};
11use serde_json::Value;
12use std::fs;
13use std::process::Command;
14
15pub fn is_detached_session_alive(record: &ExecutionRecord) -> Option<bool> {
18 let session_name = record.options.get("sessionName")?.as_str()?;
19 let isolation_mode = record.options.get("isolationMode")?.as_str()?;
20 let isolated = record.options.get("isolated")?.as_str()?;
21
22 if isolation_mode != "detached" {
23 return None;
24 }
25
26 match isolated {
27 "screen" => {
28 let output = Command::new("screen").args(["-ls"]).output().ok()?;
29 let stdout = String::from_utf8_lossy(&output.stdout);
30 Some(stdout.contains(session_name))
31 }
32 "tmux" => {
33 let status = Command::new("tmux")
34 .args(["has-session", "-t", session_name])
35 .output()
36 .ok()?;
37 Some(status.status.success())
38 }
39 "docker" => {
40 let output = Command::new("docker")
41 .args(["inspect", "-f", "{{.State.Running}}", session_name])
42 .output()
43 .ok()?;
44 let stdout = String::from_utf8_lossy(&output.stdout);
45 Some(stdout.trim() == "true")
46 }
47 "ssh" => {
48 #[cfg(unix)]
50 {
51 if let Some(pid) = record.pid {
52 let result = unsafe { libc::kill(pid as i32, 0) };
53 Some(result == 0)
54 } else {
55 None
56 }
57 }
58 #[cfg(not(unix))]
59 {
60 let _ = record.pid;
61 None
62 }
63 }
64 _ => None,
65 }
66}
67
68fn read_exit_code_from_log(log_path: &str) -> Option<i32> {
69 let content = fs::read_to_string(log_path).ok()?;
70 content
71 .lines()
72 .rev()
73 .find_map(|line| line.trim().strip_prefix("Exit Code:"))
74 .and_then(|value| value.trim().parse::<i32>().ok())
75}
76
77pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
82 let alive = match is_detached_session_alive(record) {
83 Some(v) => v,
84 None => return record.clone(),
85 };
86
87 let mut enriched = record.clone();
88
89 if alive && enriched.status == ExecutionStatus::Executed {
90 enriched.status = ExecutionStatus::Executing;
92 enriched.exit_code = None;
93 enriched.end_time = None;
94 } else if !alive && enriched.status == ExecutionStatus::Executing {
95 enriched.status = ExecutionStatus::Executed;
97 if enriched.exit_code.is_none() {
98 enriched.exit_code = Some(read_exit_code_from_log(&enriched.log_path).unwrap_or(-1));
99 }
100 if enriched.end_time.is_none() {
101 enriched.end_time = Some(chrono::Utc::now().to_rfc3339());
102 }
103 }
104
105 enriched
106}
107
108pub fn attach_current_time(record: &ExecutionRecord) -> Option<String> {
113 if record.status == ExecutionStatus::Executing {
114 Some(chrono::Utc::now().to_rfc3339())
115 } else {
116 None
117 }
118}
119
120pub fn format_record_as_links_notation(record: &ExecutionRecord) -> String {
132 format_record_as_links_notation_with_current_time(record, None)
133}
134
135pub fn format_record_as_links_notation_with_current_time(
138 record: &ExecutionRecord,
139 current_time: Option<&str>,
140) -> String {
141 format_record_as_links_notation_with_enrichments(record, current_time, None)
142}
143
144fn append_links_value(lines: &mut Vec<String>, key: &str, value: &Value, indent: usize) {
145 let prefix = " ".repeat(indent);
146 match value {
147 Value::Object(map) => {
148 if map.is_empty() {
149 return;
150 }
151 lines.push(format!("{}{}", prefix, key));
152 for (child_key, child_value) in map {
153 if !child_value.is_null() {
154 append_links_value(lines, child_key, child_value, indent + 2);
155 }
156 }
157 }
158 _ => lines.push(format!(
159 "{}{} {}",
160 prefix,
161 key,
162 format_value_for_links_notation(value)
163 )),
164 }
165}
166
167fn format_record_as_links_notation_with_enrichments(
168 record: &ExecutionRecord,
169 current_time: Option<&str>,
170 process_ids: Option<&Value>,
171) -> String {
172 let json = record.to_json();
173 let mut lines = vec![record.uuid.clone()];
174
175 if let Value::Object(map) = json {
176 for (key, value) in map {
177 if !value.is_null() {
178 if key == "options" {
179 if let Value::Object(opts) = &value {
181 if !opts.is_empty() {
182 lines.push(" options".to_string());
183 for (opt_key, opt_value) in opts {
184 if !opt_value.is_null() {
185 let formatted = format_value_for_links_notation(opt_value);
186 lines.push(format!(" {} {}", opt_key, formatted));
187 }
188 }
189 }
190 }
191 } else {
192 let formatted_value = match &value {
193 Value::String(s) => escape_for_links_notation(s),
194 Value::Bool(b) => b.to_string(),
195 Value::Number(n) => n.to_string(),
196 Value::Null => "null".to_string(),
197 Value::Object(_) | Value::Array(_) => {
198 format_value_for_links_notation(&value)
200 }
201 };
202 lines.push(format!(" {} {}", key, formatted_value));
203 }
204 }
205
206 if key == "pid" {
209 if let Some(process_ids) = process_ids {
210 append_links_value(&mut lines, "processIds", process_ids, 2);
211 }
212 }
213
214 if key == "startTime" {
216 if let Some(ct) = current_time {
217 lines.push(format!(" currentTime {}", escape_for_links_notation(ct)));
218 }
219 }
220 }
221 }
222
223 lines.join("\n")
224}
225
226pub fn format_record_as_text(record: &ExecutionRecord) -> String {
228 format_record_as_text_with_current_time(record, None)
229}
230
231pub fn format_record_as_text_with_current_time(
234 record: &ExecutionRecord,
235 current_time: Option<&str>,
236) -> String {
237 format_record_as_text_with_enrichments(record, current_time, None)
238}
239
240fn append_text_process_ids(lines: &mut Vec<String>, process_ids: &Value) {
241 let Value::Object(map) = process_ids else {
242 return;
243 };
244 if map.is_empty() {
245 return;
246 }
247
248 lines.push("Process IDs:".to_string());
249 for (key, value) in map {
250 let value_str = match value {
251 Value::String(s) => s.clone(),
252 Value::Bool(b) => b.to_string(),
253 Value::Number(n) => n.to_string(),
254 Value::Null => "null".to_string(),
255 other => serde_json::to_string(other).unwrap_or_default(),
256 };
257 lines.push(format!(" {}: {}", key, value_str));
258 }
259}
260
261fn format_record_as_text_with_enrichments(
262 record: &ExecutionRecord,
263 current_time: Option<&str>,
264 process_ids: Option<&Value>,
265) -> String {
266 let exit_code_str = record
267 .exit_code
268 .map(|c| c.to_string())
269 .unwrap_or_else(|| "N/A".to_string());
270 let pid_str = record
271 .pid
272 .map(|p| p.to_string())
273 .unwrap_or_else(|| "N/A".to_string());
274 let end_time_str = record.end_time.as_deref().unwrap_or("N/A");
275
276 let mut lines = vec![
277 "Execution Status".to_string(),
278 "=".repeat(50),
279 format!("UUID: {}", record.uuid),
280 format!("Status: {}", record.status),
281 format!("Command: {}", record.command),
282 format!("Exit Code: {}", exit_code_str),
283 format!("PID: {}", pid_str),
284 ];
285 if let Some(process_ids) = process_ids {
286 append_text_process_ids(&mut lines, process_ids);
287 }
288 lines.extend([
289 format!("Working Directory: {}", record.working_directory),
290 format!("Shell: {}", record.shell),
291 format!("Platform: {}", record.platform),
292 format!("Start Time: {}", record.start_time),
293 ]);
294 if let Some(ct) = current_time {
295 lines.push(format!("Current Time: {}", ct));
296 }
297 lines.push(format!("End Time: {}", end_time_str));
298 lines.push(format!("Log Path: {}", record.log_path));
299
300 if !record.options.is_empty() {
302 lines.push("Options:".to_string());
303 for (key, value) in &record.options {
304 let value_str = match value {
305 Value::String(s) => s.clone(),
306 Value::Bool(b) => b.to_string(),
307 Value::Number(n) => n.to_string(),
308 Value::Null => "null".to_string(),
309 other => serde_json::to_string(other).unwrap_or_default(),
310 };
311 lines.push(format!(" {}: {}", key, value_str));
312 }
313 }
314
315 lines.join("\n")
316}
317
318fn record_json_with_enrichments(
319 record: &ExecutionRecord,
320 current_time: Option<&str>,
321 process_ids: Option<&Value>,
322) -> Value {
323 let mut json = record.to_json();
324 if let Value::Object(map) = &mut json {
325 if let Some(process_ids) = process_ids {
326 map.insert("processIds".to_string(), process_ids.clone());
327 }
328 if let Some(ct) = current_time {
329 map.insert("currentTime".to_string(), Value::String(ct.to_string()));
330 }
331 }
332 json
333}
334
335pub fn format_record(record: &ExecutionRecord, format: &str) -> Result<String, String> {
337 format_record_with_current_time(record, format, None)
338}
339
340pub fn format_record_with_current_time(
344 record: &ExecutionRecord,
345 format: &str,
346 current_time: Option<&str>,
347) -> Result<String, String> {
348 format_record_with_enrichments(record, format, current_time, None)
349}
350
351fn format_record_with_enrichments(
352 record: &ExecutionRecord,
353 format: &str,
354 current_time: Option<&str>,
355 process_ids: Option<&Value>,
356) -> Result<String, String> {
357 match format {
358 "links-notation" => Ok(format_record_as_links_notation_with_enrichments(
359 record,
360 current_time,
361 process_ids,
362 )),
363 "json" => serde_json::to_string_pretty(&record_json_with_enrichments(
364 record,
365 current_time,
366 process_ids,
367 ))
368 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
369 "text" => Ok(format_record_as_text_with_enrichments(
370 record,
371 current_time,
372 process_ids,
373 )),
374 _ => Err(format!("Unknown output format: {}", format)),
375 }
376}
377
378fn sort_records_by_start_time_desc(records: &mut [ExecutionRecord]) {
379 records.sort_by(|a, b| b.start_time.cmp(&a.start_time));
380}
381
382fn indent_block(block: &str, spaces: usize) -> String {
383 let prefix = " ".repeat(spaces);
384 block
385 .lines()
386 .map(|line| format!("{}{}", prefix, line))
387 .collect::<Vec<_>>()
388 .join("\n")
389}
390
391pub fn format_record_list_as_links_notation(records: &[ExecutionRecord]) -> String {
393 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
394 let process_ids = vec![None; records.len()];
395 format_record_list_as_links_notation_with_current_times(records, ¤t_times, &process_ids)
396}
397
398fn format_record_list_as_links_notation_with_current_times(
399 records: &[ExecutionRecord],
400 current_times: &[Option<String>],
401 process_ids: &[Option<Value>],
402) -> String {
403 let mut lines = vec![
404 "executions".to_string(),
405 format!(" count {}", records.len()),
406 ];
407
408 if records.is_empty() {
409 lines.push(" records ()".to_string());
410 return lines.join("\n");
411 }
412
413 lines.push(" records".to_string());
414 for ((record, current_time), process_ids) in records
415 .iter()
416 .zip(current_times.iter())
417 .zip(process_ids.iter())
418 {
419 let block = format_record_as_links_notation_with_enrichments(
420 record,
421 current_time.as_deref(),
422 process_ids.as_ref(),
423 );
424 lines.push(indent_block(&block, 4));
425 }
426
427 lines.join("\n")
428}
429
430pub fn format_record_list_as_text(records: &[ExecutionRecord]) -> String {
432 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
433 let process_ids = vec![None; records.len()];
434 format_record_list_as_text_with_current_times(records, ¤t_times, &process_ids)
435}
436
437fn format_record_list_as_text_with_current_times(
438 records: &[ExecutionRecord],
439 current_times: &[Option<String>],
440 process_ids: &[Option<Value>],
441) -> String {
442 let mut lines = vec![
443 "Executions".to_string(),
444 "=".repeat(50),
445 format!("Count: {}", records.len()),
446 ];
447
448 for ((record, current_time), process_ids) in records
449 .iter()
450 .zip(current_times.iter())
451 .zip(process_ids.iter())
452 {
453 lines.push(String::new());
454 lines.push(format_record_as_text_with_enrichments(
455 record,
456 current_time.as_deref(),
457 process_ids.as_ref(),
458 ));
459 }
460
461 lines.join("\n")
462}
463
464fn record_list_json_with_current_times(
465 records: &[ExecutionRecord],
466 current_times: &[Option<String>],
467 process_ids: &[Option<Value>],
468) -> Value {
469 let executions: Vec<Value> = records
470 .iter()
471 .zip(current_times.iter())
472 .zip(process_ids.iter())
473 .map(|((record, current_time), process_ids)| {
474 record_json_with_enrichments(record, current_time.as_deref(), process_ids.as_ref())
475 })
476 .collect();
477
478 serde_json::json!({
479 "count": records.len(),
480 "executions": executions,
481 })
482}
483
484pub fn format_record_list(records: &[ExecutionRecord], format: &str) -> Result<String, String> {
486 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
487 let process_ids = vec![None; records.len()];
488 format_record_list_with_current_times(records, format, ¤t_times, &process_ids)
489}
490
491fn format_record_list_with_current_times(
492 records: &[ExecutionRecord],
493 format: &str,
494 current_times: &[Option<String>],
495 process_ids: &[Option<Value>],
496) -> Result<String, String> {
497 match format {
498 "links-notation" => Ok(format_record_list_as_links_notation_with_current_times(
499 records,
500 current_times,
501 process_ids,
502 )),
503 "json" => serde_json::to_string_pretty(&record_list_json_with_current_times(
504 records,
505 current_times,
506 process_ids,
507 ))
508 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
509 "text" => Ok(format_record_list_as_text_with_current_times(
510 records,
511 current_times,
512 process_ids,
513 )),
514 _ => Err(format!("Unknown output format: {}", format)),
515 }
516}
517
518pub struct StatusQueryResult {
520 pub success: bool,
521 pub output: Option<String>,
522 pub error: Option<String>,
523}
524
525pub fn list_executions(
527 store: Option<&ExecutionStore>,
528 output_format: Option<&str>,
529) -> StatusQueryResult {
530 let store = match store {
531 Some(s) => s,
532 None => {
533 return StatusQueryResult {
534 success: false,
535 output: None,
536 error: Some("Execution tracking is disabled.".to_string()),
537 }
538 }
539 };
540
541 let mut records: Vec<ExecutionRecord> =
542 store.get_all().iter().map(enrich_detached_status).collect();
543 sort_records_by_start_time_desc(&mut records);
544 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
545 let process_ids: Vec<Option<Value>> = records.iter().map(collect_process_ids).collect();
546 let format = output_format.unwrap_or("links-notation");
547
548 match format_record_list_with_current_times(&records, format, ¤t_times, &process_ids) {
549 Ok(output) => StatusQueryResult {
550 success: true,
551 output: Some(output),
552 error: None,
553 },
554 Err(e) => StatusQueryResult {
555 success: false,
556 output: None,
557 error: Some(e),
558 },
559 }
560}
561
562pub fn query_status(
564 store: Option<&ExecutionStore>,
565 identifier: &str,
566 output_format: Option<&str>,
567) -> StatusQueryResult {
568 let store = match store {
569 Some(s) => s,
570 None => {
571 return StatusQueryResult {
572 success: false,
573 output: None,
574 error: Some("Execution tracking is disabled.".to_string()),
575 }
576 }
577 };
578
579 let record = match store.get(identifier) {
580 Some(r) => r,
581 None => {
582 return StatusQueryResult {
583 success: false,
584 output: None,
585 error: Some(format!(
586 "No execution found with UUID or session name: {}",
587 identifier
588 )),
589 }
590 }
591 };
592
593 let enriched = enrich_detached_status(&record);
595 let current_time = attach_current_time(&enriched);
597 let process_ids = collect_process_ids(&enriched);
598
599 let format = output_format.unwrap_or("links-notation");
600 match format_record_with_enrichments(
601 &enriched,
602 format,
603 current_time.as_deref(),
604 process_ids.as_ref(),
605 ) {
606 Ok(output) => StatusQueryResult {
607 success: true,
608 output: Some(output),
609 error: None,
610 },
611 Err(e) => StatusQueryResult {
612 success: false,
613 output: None,
614 error: Some(e),
615 },
616 }
617}