1use crate::execution_attempt::{read_attempt_activity, read_attempt_log_tail};
9use crate::execution_control::collect_process_ids;
10use crate::execution_store::{ExecutionRecord, ExecutionStatus, ExecutionStore};
11use crate::exit_reason::{
12 describe_exit_code, resolve_exit_reason, resolve_memory_exhaustion, CGROUP_OOM_EXIT_REASON,
13};
14use crate::isolation::isolation_log::FATAL_MARKER_TAIL_BYTES;
15use crate::output_blocks::{escape_for_links_notation, format_value_for_links_notation};
16use crate::status_footer::{parse_footer_from_tail, read_footer_from_log};
17use crate::status_probe::{
18 apply_end_time, backend_exit_code, is_detached_docker_record, is_detached_session_alive,
19 read_docker_state, resolve_oom_observation,
20};
21use chrono::Utc;
22use serde_json::Value;
23
24pub fn read_exit_code_from_log(log_path: &str) -> Option<i32> {
30 read_footer_from_log(log_path).exit_code
31}
32
33pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
42 let mut enriched =
43 crate::container_removal::attach_container_removal(resolve_detached_status(record));
44 if let Some(state) = crate::cpu_penalty_monitor::read_state(record) {
45 enriched.cpu_penalty = Some(state);
46 }
47 read_attempt_activity(&mut enriched);
48 enriched.attempt_history = enrich_attempt_history(record);
49 let tail = crate::exit_evidence::record_tail(
50 &enriched,
51 read_attempt_log_tail(&enriched, FATAL_MARKER_TAIL_BYTES),
52 );
53 attach_exit_diagnostics(enriched, tail.as_deref())
54}
55
56fn attach_exit_diagnostics(mut enriched: ExecutionRecord, tail: Option<&str>) -> ExecutionRecord {
57 if enriched.status != ExecutionStatus::Executed {
58 return enriched;
59 }
60 let oom_kills = enriched.cgroup_memory.and_then(|memory| memory.oom_kills);
61 if let Some(reason) =
64 resolve_exit_reason(enriched.exit_code, tail, enriched.oom_killed, oom_kills)
65 {
66 enriched.exit_reason = Some(reason);
67 } else if enriched.exit_reason.as_deref() == Some(CGROUP_OOM_EXIT_REASON) {
68 enriched.exit_reason = None;
69 }
70 if enriched
71 .memory_exhausted_reason
72 .as_deref()
73 .is_some_and(|s| {
74 s.starts_with("Docker reported")
75 || s.starts_with("cgroup memory.events")
76 || s.starts_with("Exit-time evidence")
77 })
78 {
79 enriched.memory_exhausted = None;
80 enriched.memory_exhausted_reason = None;
81 }
82 if enriched.memory_exhausted.is_none() {
83 if let Some(memory) =
84 resolve_memory_exhaustion(enriched.exit_code, tail, enriched.oom_killed, oom_kills)
85 {
86 enriched.memory_exhausted = Some(memory.memory_exhausted);
87 enriched.memory_exhausted_reason = Some(memory.memory_exhausted_reason);
88 }
89 }
90 enriched
91}
92
93fn enrich_attempt_history(record: &ExecutionRecord) -> Vec<Value> {
94 let mut base = record.clone();
95 base.attempt_history.clear();
96 record
97 .attempt_history
98 .iter()
99 .enumerate()
100 .map(|(index, attempt)| {
101 let mut historical = base.clone();
102 historical.status = if attempt.get("status").and_then(Value::as_str) == Some("executed")
103 {
104 ExecutionStatus::Executed
105 } else {
106 ExecutionStatus::Executing
107 };
108 historical.exit_code = attempt
109 .get("exitCode")
110 .and_then(Value::as_i64)
111 .and_then(|n| i32::try_from(n).ok());
112 historical.oom_killed = attempt.get("oomKilled").and_then(Value::as_bool);
113 historical.cgroup_memory = attempt
114 .get("cgroupMemory")
115 .and_then(|v| serde_json::from_value(v.clone()).ok());
116 historical.exit_reason = attempt
117 .get("exitReason")
118 .and_then(Value::as_str)
119 .map(str::to_string);
120 historical.memory_exhausted = attempt.get("memoryExhausted").and_then(Value::as_bool);
121 historical.memory_exhausted_reason = attempt
122 .get("memoryExhaustedReason")
123 .and_then(Value::as_str)
124 .map(str::to_string);
125 historical.options.remove("exitEvidence");
126 if let Some(evidence) = attempt.get("exitEvidence") {
127 historical
128 .options
129 .insert("exitEvidence".into(), evidence.clone());
130 }
131 let end = if let Some(next) = record.attempt_history.get(index + 1) {
132 next.get("logOffset").and_then(Value::as_u64)
133 } else {
134 record.attempt.as_ref().and_then(|a| a.log_offset)
135 };
136 let tail = attempt
137 .get("logOffset")
138 .and_then(Value::as_u64)
139 .zip(end)
140 .filter(|(start, end)| end >= start)
141 .and_then(|(start, end)| {
142 crate::isolation::isolation_log::read_log_tail_range(
143 &record.log_path,
144 FATAL_MARKER_TAIL_BYTES,
145 start,
146 end,
147 )
148 });
149 let tail = crate::exit_evidence::record_tail(&historical, tail);
150 let enriched = attach_exit_diagnostics(historical, tail.as_deref()).to_json();
151 let mut output = attempt.clone();
152 for key in ["exitReason", "memoryExhausted", "memoryExhaustedReason"] {
153 if let Some(object) = output.as_object_mut() {
154 object.remove(key);
155 }
156 if let Some(value) = enriched.get(key) {
157 output[key] = value.clone();
158 }
159 }
160 output
161 })
162 .collect()
163}
164
165fn resolve_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
170 if crate::launch_owner::has_active_launch(record) {
171 return record.clone();
172 }
173 let tail = read_attempt_log_tail(record, FATAL_MARKER_TAIL_BYTES);
174 let footer = parse_footer_from_tail(tail.as_deref().unwrap_or(""));
175 let footer_exit = footer.exit_code;
176 let is_detached_docker = is_detached_docker_record(record);
177 let docker_state = if is_detached_docker {
178 read_docker_state(record)
179 } else {
180 None
181 };
182
183 let oom_killed = resolve_oom_observation(record, docker_state.as_ref());
185 let clone_record = || {
186 let mut enriched = record.clone();
187 if oom_killed.is_some() {
188 enriched.oom_killed = oom_killed;
189 }
190 enriched
191 };
192
193 let alive = if is_detached_docker {
194 docker_state.as_ref().map(|state| state.running)
195 } else {
196 is_detached_session_alive(record)
197 };
198
199 let alive = match alive {
200 Some(value) => value,
201 None => {
202 let is_detached =
210 record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached");
211 if is_detached && record.status == ExecutionStatus::Executing {
212 if footer_exit.is_some() {
213 let mut enriched = clone_record();
214 enriched.status = ExecutionStatus::Executed;
215 enriched.exit_code = footer_exit;
216 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
217 return enriched;
218 }
219 if oom_killed == Some(true) {
220 let mut enriched = clone_record();
227 enriched.status = ExecutionStatus::Executed;
228 enriched.exit_code = Some(137);
229 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
230 return enriched;
231 }
232 }
233 return clone_record();
234 }
235 };
236
237 let mut enriched = clone_record();
238
239 if alive && enriched.status == ExecutionStatus::Executed {
240 if enriched.exit_code.is_none() && footer_exit.is_none() {
247 enriched.status = ExecutionStatus::Executing;
249 enriched.exit_code = None;
250 enriched.end_time = None;
251 enriched.end_time_source = None;
252 }
253 } else if !alive && enriched.status == ExecutionStatus::Executing {
255 enriched.status = ExecutionStatus::Executed;
263 if enriched.exit_code.is_none() {
264 enriched.exit_code = Some(
265 backend_exit_code(docker_state.as_ref())
266 .or(footer_exit)
267 .or(if oom_killed == Some(true) {
268 Some(137)
269 } else {
270 None
271 })
272 .unwrap_or(-1),
273 );
274 }
275 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
276 }
277
278 enriched
279}
280
281pub fn attach_current_time(record: &ExecutionRecord) -> Option<String> {
286 if record.status == ExecutionStatus::Executing {
287 Some(Utc::now().to_rfc3339())
288 } else {
289 None
290 }
291}
292
293pub fn format_record_as_links_notation(record: &ExecutionRecord) -> String {
305 format_record_as_links_notation_with_current_time(record, None)
306}
307
308pub fn format_record_as_links_notation_with_current_time(
311 record: &ExecutionRecord,
312 current_time: Option<&str>,
313) -> String {
314 format_record_as_links_notation_with_enrichments(record, current_time, None, &[])
315}
316
317fn append_links_array(lines: &mut Vec<String>, values: &[Value], indent: usize) {
318 let prefix = " ".repeat(indent);
319 if values.is_empty() {
320 lines.push(format!("{}()", prefix));
321 return;
322 }
323
324 lines.push(format!("{}(", prefix));
325 for value in values {
326 match value {
327 Value::Array(nested) => append_links_array(lines, nested, indent + 2),
328 Value::Object(map) => {
329 for (child_key, child_value) in map {
330 if !child_value.is_null() {
331 append_links_value(lines, child_key, child_value, indent + 2);
332 }
333 }
334 }
335 _ => lines.push(format!(
336 "{}{}",
337 " ".repeat(indent + 2),
338 format_value_for_links_notation(value)
339 )),
340 }
341 }
342 lines.push(format!("{})", prefix));
343}
344
345fn append_links_value(lines: &mut Vec<String>, key: &str, value: &Value, indent: usize) {
346 let prefix = " ".repeat(indent);
347 match value {
348 Value::Object(map) => {
349 if map.is_empty() {
350 return;
351 }
352 lines.push(format!("{}{}", prefix, key));
353 for (child_key, child_value) in map {
354 if !child_value.is_null() {
355 append_links_value(lines, child_key, child_value, indent + 4);
356 }
357 }
358 }
359 Value::Array(values) => {
360 lines.push(format!("{}{}", prefix, key));
361 append_links_array(lines, values, indent + 2);
362 }
363 _ => lines.push(format!(
364 "{}{} {}",
365 prefix,
366 key,
367 format_value_for_links_notation(value)
368 )),
369 }
370}
371
372fn format_record_as_links_notation_with_enrichments(
373 record: &ExecutionRecord,
374 current_time: Option<&str>,
375 process_ids: Option<&Value>,
376 identity: &[(String, Value)],
377) -> String {
378 let json = record.to_json();
379 let mut lines = vec![record.uuid.clone()];
380
381 if let Value::Object(map) = json {
382 for (key, value) in map {
383 if !value.is_null() {
384 if key == "options" {
385 if let Value::Object(opts) = &value {
389 if !opts.is_empty() {
390 lines.push(" options".to_string());
391 for (opt_key, opt_value) in opts {
392 if !opt_value.is_null() {
393 append_links_value(&mut lines, opt_key, opt_value, 4);
394 }
395 }
396 }
397 }
398 } else if value.is_object() || value.is_array() {
399 append_links_value(&mut lines, &key, &value, 2);
402 } else {
403 let formatted_value = match &value {
404 Value::String(s) => escape_for_links_notation(s),
405 Value::Bool(b) => b.to_string(),
406 Value::Number(n) => n.to_string(),
407 Value::Null => "null".to_string(),
408 Value::Object(_) | Value::Array(_) => {
409 format_value_for_links_notation(&value)
411 }
412 };
413 lines.push(format!(" {} {}", key, formatted_value));
414 }
415 }
416
417 if key == "uuid" {
419 for (field, value) in identity {
420 append_links_value(&mut lines, field, value, 2);
421 }
422 }
423
424 if key == "pid" {
427 if let Some(process_ids) = process_ids {
428 append_links_value(&mut lines, "processIds", process_ids, 2);
429 }
430 }
431
432 if key == "startTime" {
434 if let Some(ct) = current_time {
435 lines.push(format!(" currentTime {}", escape_for_links_notation(ct)));
436 }
437 }
438 }
439 }
440
441 lines.join("\n")
442}
443
444pub fn format_record_as_text(record: &ExecutionRecord) -> String {
446 format_record_as_text_with_current_time(record, None)
447}
448
449pub fn format_record_as_text_with_current_time(
452 record: &ExecutionRecord,
453 current_time: Option<&str>,
454) -> String {
455 format_record_as_text_with_enrichments(record, current_time, None, &[])
456}
457
458fn append_text_process_ids(lines: &mut Vec<String>, process_ids: &Value) {
459 let Value::Object(map) = process_ids else {
460 return;
461 };
462 if map.is_empty() {
463 return;
464 }
465
466 lines.push("Process IDs:".to_string());
467 for (key, value) in map {
468 let value_str = match value {
469 Value::String(s) => s.clone(),
470 Value::Bool(b) => b.to_string(),
471 Value::Number(n) => n.to_string(),
472 Value::Null => "null".to_string(),
473 other => serde_json::to_string(other).unwrap_or_default(),
474 };
475 lines.push(format!(" {}: {}", key, value_str));
476 }
477}
478
479fn format_record_as_text_with_enrichments(
480 record: &ExecutionRecord,
481 current_time: Option<&str>,
482 process_ids: Option<&Value>,
483 identity: &[(String, Value)],
484) -> String {
485 let exit_code_str = record
486 .exit_code
487 .map(|c| describe_exit_code(Some(c)).text)
491 .unwrap_or_else(|| "N/A".to_string());
492 let pid_str = record
493 .pid
494 .map(|p| p.to_string())
495 .unwrap_or_else(|| "N/A".to_string());
496 let end_time_str = record.end_time.as_deref().unwrap_or("N/A");
497
498 let mut lines = vec![
499 "Execution Status".to_string(),
500 "=".repeat(50),
501 format!("UUID: {}", record.uuid),
502 format!("Status: {}", record.status),
503 format!("Command: {}", record.command),
504 ];
505 lines.extend(crate::execution_chain::identity_text_lines(
506 record, identity,
507 ));
508 lines.push(format!("Exit Code: {}", exit_code_str));
509 if let Some(oom_killed) = record.oom_killed {
510 lines.push(format!("OOM Killed: {}", oom_killed));
511 }
512 if let Some(ref exit_reason) = record.exit_reason {
513 lines.push(format!("Exit Reason: {}", exit_reason));
514 }
515 if let Some(memory_exhausted) = record.memory_exhausted {
516 lines.push(format!("Memory Exhausted: {}", memory_exhausted));
517 }
518 if let Some(ref reason) = record.memory_exhausted_reason {
519 lines.push(format!("Memory Evidence: {}", reason));
520 }
521 if let Some(ref memory) = record.cgroup_memory {
523 lines.push(format!(
524 "Cgroup Memory: {}",
525 crate::cgroup_memory::format_cgroup_memory(memory)
526 ));
527 }
528 if let Some(ref state) = record.cpu_penalty {
529 lines.push(format!("CPU Penalty: {}", state));
530 }
531 lines.push(format!("PID: {}", pid_str));
532 if let Some(process_ids) = process_ids {
533 append_text_process_ids(&mut lines, process_ids);
534 }
535 lines.extend([
536 format!("Working Directory: {}", record.working_directory),
537 format!("Shell: {}", record.shell),
538 format!("Platform: {}", record.platform),
539 format!("Start Time: {}", record.start_time),
540 ]);
541 if let Some(ct) = current_time {
542 lines.push(format!("Current Time: {}", ct));
543 }
544 lines.push(format!("End Time: {}", end_time_str));
545 if let Some(ref source) = record.end_time_source {
546 lines.push(format!("End Time Source: {}", source));
549 }
550 if let Some(ref started) = record.container_started_at {
551 lines.push(format!("Container Started: {}", started));
552 }
553 if let Some(ref detected) = record.stale_detected_at {
554 lines.push(format!("Stale Detected At: {}", detected));
555 }
556 if let Some(ref state) = record.container_state {
557 lines.push(format!("Container State: {}", state));
559 lines.push(format!(
560 "Container Removed: {}",
561 record
562 .container_removed_observed_at
563 .as_deref()
564 .unwrap_or("N/A")
565 ));
566 }
567 lines.push(format!("Log Path: {}", record.log_path));
568 if let Some(attempt) = &record.attempt {
569 lines.extend([
570 format!("Attempt: {}", attempt.number),
571 format!("Attempt Started: {}", attempt.started_at),
572 format!(
573 "Attempt Log Offset: {}",
574 attempt
575 .log_offset
576 .map(|offset| offset.to_string())
577 .unwrap_or_else(|| "N/A".into())
578 ),
579 format!(
580 "Launch Accepted: {}",
581 attempt.launch_accepted_at.as_deref().unwrap_or("N/A")
582 ),
583 format!(
584 "Watcher Attached: {}",
585 attempt.watcher_attached_at.as_deref().unwrap_or("N/A")
586 ),
587 format!(
588 "Last Output: {}",
589 attempt.last_output_at.as_deref().unwrap_or("N/A")
590 ),
591 ]);
592 }
593
594 if !record.options.is_empty() {
596 lines.push("Options:".to_string());
597 let mut options: Vec<_> = record.options.iter().collect();
600 options.sort_by(|a, b| a.0.cmp(b.0));
601 for (key, value) in options {
602 let value_str = match value {
603 Value::String(s) => s.clone(),
604 Value::Bool(b) => b.to_string(),
605 Value::Number(n) => n.to_string(),
606 Value::Null => "null".to_string(),
607 other => serde_json::to_string(other).unwrap_or_default(),
608 };
609 lines.push(format!(" {}: {}", key, value_str));
610 }
611 }
612
613 lines.join("\n")
614}
615
616fn record_json_with_enrichments(
617 record: &ExecutionRecord,
618 current_time: Option<&str>,
619 process_ids: Option<&Value>,
620 identity: &[(String, Value)],
621) -> Value {
622 let mut json = record.to_json();
623 if let Value::Object(map) = &mut json {
624 map.extend(identity.iter().cloned());
625 if let Some(process_ids) = process_ids {
626 map.insert("processIds".to_string(), process_ids.clone());
627 }
628 if let Some(ct) = current_time {
629 map.insert("currentTime".to_string(), Value::String(ct.to_string()));
630 }
631 }
632 json
633}
634
635pub fn format_record(record: &ExecutionRecord, format: &str) -> Result<String, String> {
637 format_record_with_current_time(record, format, None)
638}
639
640pub fn format_record_with_current_time(
644 record: &ExecutionRecord,
645 format: &str,
646 current_time: Option<&str>,
647) -> Result<String, String> {
648 format_record_with_enrichments(record, format, current_time, None, &[])
649}
650
651fn format_record_with_enrichments(
652 record: &ExecutionRecord,
653 format: &str,
654 current_time: Option<&str>,
655 process_ids: Option<&Value>,
656 identity: &[(String, Value)],
657) -> Result<String, String> {
658 match format {
659 "links-notation" => Ok(format_record_as_links_notation_with_enrichments(
660 record,
661 current_time,
662 process_ids,
663 identity,
664 )),
665 "json" => serde_json::to_string_pretty(&record_json_with_enrichments(
666 record,
667 current_time,
668 process_ids,
669 identity,
670 ))
671 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
672 "text" => Ok(format_record_as_text_with_enrichments(
673 record,
674 current_time,
675 process_ids,
676 identity,
677 )),
678 _ => Err(format!("Unknown output format: {}", format)),
679 }
680}
681
682fn sort_records_by_start_time_desc(records: &mut [ExecutionRecord]) {
683 records.sort_by(|a, b| b.start_time.cmp(&a.start_time));
684}
685
686fn indent_block(block: &str, spaces: usize) -> String {
687 let prefix = " ".repeat(spaces);
688 block
689 .lines()
690 .map(|line| format!("{}{}", prefix, line))
691 .collect::<Vec<_>>()
692 .join("\n")
693}
694
695pub fn format_record_list_as_links_notation(records: &[ExecutionRecord]) -> String {
697 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
698 let process_ids = vec![None; records.len()];
699 format_record_list_as_links_notation_with_current_times(records, ¤t_times, &process_ids)
700}
701
702fn format_record_list_as_links_notation_with_current_times(
703 records: &[ExecutionRecord],
704 current_times: &[Option<String>],
705 process_ids: &[Option<Value>],
706) -> String {
707 let mut lines = vec![
708 "executions".to_string(),
709 format!(" count {}", records.len()),
710 ];
711
712 if records.is_empty() {
713 lines.push(" records ()".to_string());
714 return lines.join("\n");
715 }
716
717 lines.push(" records".to_string());
718 for ((record, current_time), process_ids) in records
719 .iter()
720 .zip(current_times.iter())
721 .zip(process_ids.iter())
722 {
723 let block = format_record_as_links_notation_with_enrichments(
724 record,
725 current_time.as_deref(),
726 process_ids.as_ref(),
727 &[],
728 );
729 lines.push(indent_block(&block, 4));
730 }
731
732 lines.join("\n")
733}
734
735pub fn format_record_list_as_text(records: &[ExecutionRecord]) -> String {
737 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
738 let process_ids = vec![None; records.len()];
739 format_record_list_as_text_with_current_times(records, ¤t_times, &process_ids)
740}
741
742fn format_record_list_as_text_with_current_times(
743 records: &[ExecutionRecord],
744 current_times: &[Option<String>],
745 process_ids: &[Option<Value>],
746) -> String {
747 let mut lines = vec![
748 "Executions".to_string(),
749 "=".repeat(50),
750 format!("Count: {}", records.len()),
751 ];
752
753 for ((record, current_time), process_ids) in records
754 .iter()
755 .zip(current_times.iter())
756 .zip(process_ids.iter())
757 {
758 lines.push(String::new());
759 lines.push(format_record_as_text_with_enrichments(
760 record,
761 current_time.as_deref(),
762 process_ids.as_ref(),
763 &[],
764 ));
765 }
766
767 lines.join("\n")
768}
769
770fn record_list_json_with_current_times(
771 records: &[ExecutionRecord],
772 current_times: &[Option<String>],
773 process_ids: &[Option<Value>],
774) -> Value {
775 let executions: Vec<Value> = records
776 .iter()
777 .zip(current_times.iter())
778 .zip(process_ids.iter())
779 .map(|((record, current_time), process_ids)| {
780 record_json_with_enrichments(record, current_time.as_deref(), process_ids.as_ref(), &[])
781 })
782 .collect();
783
784 serde_json::json!({
785 "count": records.len(),
786 "executions": executions,
787 })
788}
789
790pub fn format_record_list(records: &[ExecutionRecord], format: &str) -> Result<String, String> {
792 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
793 let process_ids = vec![None; records.len()];
794 format_record_list_with_current_times(records, format, ¤t_times, &process_ids)
795}
796
797fn format_record_list_with_current_times(
798 records: &[ExecutionRecord],
799 format: &str,
800 current_times: &[Option<String>],
801 process_ids: &[Option<Value>],
802) -> Result<String, String> {
803 match format {
804 "links-notation" => Ok(format_record_list_as_links_notation_with_current_times(
805 records,
806 current_times,
807 process_ids,
808 )),
809 "json" => serde_json::to_string_pretty(&record_list_json_with_current_times(
810 records,
811 current_times,
812 process_ids,
813 ))
814 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
815 "text" => Ok(format_record_list_as_text_with_current_times(
816 records,
817 current_times,
818 process_ids,
819 )),
820 _ => Err(format!("Unknown output format: {}", format)),
821 }
822}
823
824pub struct StatusQueryResult {
826 pub success: bool,
827 pub output: Option<String>,
828 pub error: Option<String>,
829}
830
831pub fn list_executions(
833 store: Option<&ExecutionStore>,
834 output_format: Option<&str>,
835) -> StatusQueryResult {
836 list_executions_filtered(store, output_format, false)
837}
838
839pub fn list_executions_filtered(
845 store: Option<&ExecutionStore>,
846 output_format: Option<&str>,
847 running_only: bool,
848) -> StatusQueryResult {
849 let store = match store {
850 Some(s) => s,
851 None => {
852 return StatusQueryResult {
853 success: false,
854 output: None,
855 error: Some("Execution tracking is disabled.".to_string()),
856 }
857 }
858 };
859
860 let mut records: Vec<ExecutionRecord> =
861 store.get_all().iter().map(enrich_detached_status).collect();
862 if running_only {
863 records.retain(|record| record.status == ExecutionStatus::Executing);
864 }
865 sort_records_by_start_time_desc(&mut records);
866 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
867 let process_ids: Vec<Option<Value>> = records.iter().map(collect_process_ids).collect();
868 let format = output_format.unwrap_or("links-notation");
869
870 match format_record_list_with_current_times(&records, format, ¤t_times, &process_ids) {
871 Ok(output) => StatusQueryResult {
872 success: true,
873 output: Some(output),
874 error: None,
875 },
876 Err(e) => StatusQueryResult {
877 success: false,
878 output: None,
879 error: Some(e),
880 },
881 }
882}
883
884pub fn query_status(
886 store: Option<&ExecutionStore>,
887 identifier: &str,
888 output_format: Option<&str>,
889) -> StatusQueryResult {
890 let store = match store {
891 Some(s) => s,
892 None => {
893 return StatusQueryResult {
894 success: false,
895 output: None,
896 error: Some("Execution tracking is disabled.".to_string()),
897 }
898 }
899 };
900
901 let record = match store.get(identifier) {
902 Some(r) => r,
903 None => {
904 return StatusQueryResult {
905 success: false,
906 output: None,
907 error: Some(format!(
908 "No execution found with UUID or session name: {}",
909 identifier
910 )),
911 }
912 }
913 };
914
915 let enriched = enrich_detached_status(&record);
917 crate::container_removal::persist_container_removal(store, &record, &enriched);
918 let current_time = attach_current_time(&enriched);
920 let process_ids = collect_process_ids(&enriched);
921 let identity = crate::execution_chain::status_identity(&enriched, Some(store));
922
923 let format = output_format.unwrap_or("links-notation");
924 match format_record_with_enrichments(
925 &enriched,
926 format,
927 current_time.as_deref(),
928 process_ids.as_ref(),
929 &identity,
930 ) {
931 Ok(output) => StatusQueryResult {
932 success: true,
933 output: Some(output),
934 error: None,
935 },
936 Err(e) => StatusQueryResult {
937 success: false,
938 output: None,
939 error: Some(e),
940 },
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947 use crate::execution_store::ExecutionRecordOptions;
948 use serde_json::json;
949
950 fn executing_record() -> ExecutionRecord {
951 ExecutionRecord::with_options(ExecutionRecordOptions {
952 command: "sleep 60".to_string(),
953 uuid: Some("issue-126-rust".to_string()),
954 pid: Some(667105),
955 status: Some(ExecutionStatus::Executing),
956 log_path: Some("/tmp/issue-126.log".to_string()),
957 start_time: Some("2026-04-23T10:00:00Z".to_string()),
958 working_directory: Some("/home/user".to_string()),
959 shell: Some("/bin/bash".to_string()),
960 platform: Some("linux".to_string()),
961 ..Default::default()
962 })
963 }
964
965 #[test]
966 fn links_notation_indents_nested_process_id_arrays() {
967 let process_ids = json!({
968 "wrapperPid": 667105,
969 "screenPid": 667120,
970 "commandPids": [667121, 667122],
971 });
972 let output = format_record_with_enrichments(
973 &executing_record(),
974 "links-notation",
975 Some("2026-04-23T10:10:13.042Z"),
976 Some(&process_ids),
977 &[],
978 )
979 .expect("links-notation should format");
980
981 assert!(
982 output.contains(
983 " commandPids\n (\n 667121\n 667122\n )"
984 ),
985 "processIds should be a nested indented block, output: {}",
986 output
987 );
988 assert!(
989 !output.contains("\n(\n"),
990 "opening parenthesis must not start at column 1: {}",
991 output
992 );
993 }
994}