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) -> String {
377 let json = record.to_json();
378 let mut lines = vec![record.uuid.clone()];
379
380 if let Value::Object(map) = json {
381 for (key, value) in map {
382 if !value.is_null() {
383 if key == "options" {
384 if let Value::Object(opts) = &value {
388 if !opts.is_empty() {
389 lines.push(" options".to_string());
390 for (opt_key, opt_value) in opts {
391 if !opt_value.is_null() {
392 append_links_value(&mut lines, opt_key, opt_value, 4);
393 }
394 }
395 }
396 }
397 } else if value.is_object() || value.is_array() {
398 append_links_value(&mut lines, &key, &value, 2);
401 } else {
402 let formatted_value = match &value {
403 Value::String(s) => escape_for_links_notation(s),
404 Value::Bool(b) => b.to_string(),
405 Value::Number(n) => n.to_string(),
406 Value::Null => "null".to_string(),
407 Value::Object(_) | Value::Array(_) => {
408 format_value_for_links_notation(&value)
410 }
411 };
412 lines.push(format!(" {} {}", key, formatted_value));
413 }
414 }
415
416 if key == "pid" {
419 if let Some(process_ids) = process_ids {
420 append_links_value(&mut lines, "processIds", process_ids, 2);
421 }
422 }
423
424 if key == "startTime" {
426 if let Some(ct) = current_time {
427 lines.push(format!(" currentTime {}", escape_for_links_notation(ct)));
428 }
429 }
430 }
431 }
432
433 lines.join("\n")
434}
435
436pub fn format_record_as_text(record: &ExecutionRecord) -> String {
438 format_record_as_text_with_current_time(record, None)
439}
440
441pub fn format_record_as_text_with_current_time(
444 record: &ExecutionRecord,
445 current_time: Option<&str>,
446) -> String {
447 format_record_as_text_with_enrichments(record, current_time, None)
448}
449
450fn append_text_process_ids(lines: &mut Vec<String>, process_ids: &Value) {
451 let Value::Object(map) = process_ids else {
452 return;
453 };
454 if map.is_empty() {
455 return;
456 }
457
458 lines.push("Process IDs:".to_string());
459 for (key, value) in map {
460 let value_str = match value {
461 Value::String(s) => s.clone(),
462 Value::Bool(b) => b.to_string(),
463 Value::Number(n) => n.to_string(),
464 Value::Null => "null".to_string(),
465 other => serde_json::to_string(other).unwrap_or_default(),
466 };
467 lines.push(format!(" {}: {}", key, value_str));
468 }
469}
470
471fn format_record_as_text_with_enrichments(
472 record: &ExecutionRecord,
473 current_time: Option<&str>,
474 process_ids: Option<&Value>,
475) -> String {
476 let exit_code_str = record
477 .exit_code
478 .map(|c| describe_exit_code(Some(c)).text)
482 .unwrap_or_else(|| "N/A".to_string());
483 let pid_str = record
484 .pid
485 .map(|p| p.to_string())
486 .unwrap_or_else(|| "N/A".to_string());
487 let end_time_str = record.end_time.as_deref().unwrap_or("N/A");
488
489 let mut lines = vec![
490 "Execution Status".to_string(),
491 "=".repeat(50),
492 format!("UUID: {}", record.uuid),
493 format!("Status: {}", record.status),
494 format!("Command: {}", record.command),
495 format!("Exit Code: {}", exit_code_str),
496 ];
497 if let Some(oom_killed) = record.oom_killed {
498 lines.push(format!("OOM Killed: {}", oom_killed));
499 }
500 if let Some(ref exit_reason) = record.exit_reason {
501 lines.push(format!("Exit Reason: {}", exit_reason));
502 }
503 if let Some(memory_exhausted) = record.memory_exhausted {
504 lines.push(format!("Memory Exhausted: {}", memory_exhausted));
505 }
506 if let Some(ref reason) = record.memory_exhausted_reason {
507 lines.push(format!("Memory Evidence: {}", reason));
508 }
509 if let Some(ref memory) = record.cgroup_memory {
511 lines.push(format!(
512 "Cgroup Memory: {}",
513 crate::cgroup_memory::format_cgroup_memory(memory)
514 ));
515 }
516 if let Some(ref state) = record.cpu_penalty {
517 lines.push(format!("CPU Penalty: {}", state));
518 }
519 lines.push(format!("PID: {}", pid_str));
520 if let Some(process_ids) = process_ids {
521 append_text_process_ids(&mut lines, process_ids);
522 }
523 lines.extend([
524 format!("Working Directory: {}", record.working_directory),
525 format!("Shell: {}", record.shell),
526 format!("Platform: {}", record.platform),
527 format!("Start Time: {}", record.start_time),
528 ]);
529 if let Some(ct) = current_time {
530 lines.push(format!("Current Time: {}", ct));
531 }
532 lines.push(format!("End Time: {}", end_time_str));
533 if let Some(ref source) = record.end_time_source {
534 lines.push(format!("End Time Source: {}", source));
537 }
538 if let Some(ref started) = record.container_started_at {
539 lines.push(format!("Container Started: {}", started));
540 }
541 if let Some(ref detected) = record.stale_detected_at {
542 lines.push(format!("Stale Detected At: {}", detected));
543 }
544 if let Some(ref state) = record.container_state {
545 lines.push(format!("Container State: {}", state));
547 lines.push(format!(
548 "Container Removed: {}",
549 record
550 .container_removed_observed_at
551 .as_deref()
552 .unwrap_or("N/A")
553 ));
554 }
555 lines.push(format!("Log Path: {}", record.log_path));
556 if let Some(attempt) = &record.attempt {
557 lines.extend([
558 format!("Attempt: {}", attempt.number),
559 format!("Attempt Started: {}", attempt.started_at),
560 format!(
561 "Attempt Log Offset: {}",
562 attempt
563 .log_offset
564 .map(|offset| offset.to_string())
565 .unwrap_or_else(|| "N/A".into())
566 ),
567 format!(
568 "Launch Accepted: {}",
569 attempt.launch_accepted_at.as_deref().unwrap_or("N/A")
570 ),
571 format!(
572 "Watcher Attached: {}",
573 attempt.watcher_attached_at.as_deref().unwrap_or("N/A")
574 ),
575 format!(
576 "Last Output: {}",
577 attempt.last_output_at.as_deref().unwrap_or("N/A")
578 ),
579 ]);
580 }
581
582 if !record.options.is_empty() {
584 lines.push("Options:".to_string());
585 let mut options: Vec<_> = record.options.iter().collect();
588 options.sort_by(|a, b| a.0.cmp(b.0));
589 for (key, value) in options {
590 let value_str = match value {
591 Value::String(s) => s.clone(),
592 Value::Bool(b) => b.to_string(),
593 Value::Number(n) => n.to_string(),
594 Value::Null => "null".to_string(),
595 other => serde_json::to_string(other).unwrap_or_default(),
596 };
597 lines.push(format!(" {}: {}", key, value_str));
598 }
599 }
600
601 lines.join("\n")
602}
603
604fn record_json_with_enrichments(
605 record: &ExecutionRecord,
606 current_time: Option<&str>,
607 process_ids: Option<&Value>,
608) -> Value {
609 let mut json = record.to_json();
610 if let Value::Object(map) = &mut json {
611 if let Some(process_ids) = process_ids {
612 map.insert("processIds".to_string(), process_ids.clone());
613 }
614 if let Some(ct) = current_time {
615 map.insert("currentTime".to_string(), Value::String(ct.to_string()));
616 }
617 }
618 json
619}
620
621pub fn format_record(record: &ExecutionRecord, format: &str) -> Result<String, String> {
623 format_record_with_current_time(record, format, None)
624}
625
626pub fn format_record_with_current_time(
630 record: &ExecutionRecord,
631 format: &str,
632 current_time: Option<&str>,
633) -> Result<String, String> {
634 format_record_with_enrichments(record, format, current_time, None)
635}
636
637fn format_record_with_enrichments(
638 record: &ExecutionRecord,
639 format: &str,
640 current_time: Option<&str>,
641 process_ids: Option<&Value>,
642) -> Result<String, String> {
643 match format {
644 "links-notation" => Ok(format_record_as_links_notation_with_enrichments(
645 record,
646 current_time,
647 process_ids,
648 )),
649 "json" => serde_json::to_string_pretty(&record_json_with_enrichments(
650 record,
651 current_time,
652 process_ids,
653 ))
654 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
655 "text" => Ok(format_record_as_text_with_enrichments(
656 record,
657 current_time,
658 process_ids,
659 )),
660 _ => Err(format!("Unknown output format: {}", format)),
661 }
662}
663
664fn sort_records_by_start_time_desc(records: &mut [ExecutionRecord]) {
665 records.sort_by(|a, b| b.start_time.cmp(&a.start_time));
666}
667
668fn indent_block(block: &str, spaces: usize) -> String {
669 let prefix = " ".repeat(spaces);
670 block
671 .lines()
672 .map(|line| format!("{}{}", prefix, line))
673 .collect::<Vec<_>>()
674 .join("\n")
675}
676
677pub fn format_record_list_as_links_notation(records: &[ExecutionRecord]) -> String {
679 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
680 let process_ids = vec![None; records.len()];
681 format_record_list_as_links_notation_with_current_times(records, ¤t_times, &process_ids)
682}
683
684fn format_record_list_as_links_notation_with_current_times(
685 records: &[ExecutionRecord],
686 current_times: &[Option<String>],
687 process_ids: &[Option<Value>],
688) -> String {
689 let mut lines = vec![
690 "executions".to_string(),
691 format!(" count {}", records.len()),
692 ];
693
694 if records.is_empty() {
695 lines.push(" records ()".to_string());
696 return lines.join("\n");
697 }
698
699 lines.push(" records".to_string());
700 for ((record, current_time), process_ids) in records
701 .iter()
702 .zip(current_times.iter())
703 .zip(process_ids.iter())
704 {
705 let block = format_record_as_links_notation_with_enrichments(
706 record,
707 current_time.as_deref(),
708 process_ids.as_ref(),
709 );
710 lines.push(indent_block(&block, 4));
711 }
712
713 lines.join("\n")
714}
715
716pub fn format_record_list_as_text(records: &[ExecutionRecord]) -> String {
718 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
719 let process_ids = vec![None; records.len()];
720 format_record_list_as_text_with_current_times(records, ¤t_times, &process_ids)
721}
722
723fn format_record_list_as_text_with_current_times(
724 records: &[ExecutionRecord],
725 current_times: &[Option<String>],
726 process_ids: &[Option<Value>],
727) -> String {
728 let mut lines = vec![
729 "Executions".to_string(),
730 "=".repeat(50),
731 format!("Count: {}", records.len()),
732 ];
733
734 for ((record, current_time), process_ids) in records
735 .iter()
736 .zip(current_times.iter())
737 .zip(process_ids.iter())
738 {
739 lines.push(String::new());
740 lines.push(format_record_as_text_with_enrichments(
741 record,
742 current_time.as_deref(),
743 process_ids.as_ref(),
744 ));
745 }
746
747 lines.join("\n")
748}
749
750fn record_list_json_with_current_times(
751 records: &[ExecutionRecord],
752 current_times: &[Option<String>],
753 process_ids: &[Option<Value>],
754) -> Value {
755 let executions: Vec<Value> = records
756 .iter()
757 .zip(current_times.iter())
758 .zip(process_ids.iter())
759 .map(|((record, current_time), process_ids)| {
760 record_json_with_enrichments(record, current_time.as_deref(), process_ids.as_ref())
761 })
762 .collect();
763
764 serde_json::json!({
765 "count": records.len(),
766 "executions": executions,
767 })
768}
769
770pub fn format_record_list(records: &[ExecutionRecord], format: &str) -> Result<String, String> {
772 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
773 let process_ids = vec![None; records.len()];
774 format_record_list_with_current_times(records, format, ¤t_times, &process_ids)
775}
776
777fn format_record_list_with_current_times(
778 records: &[ExecutionRecord],
779 format: &str,
780 current_times: &[Option<String>],
781 process_ids: &[Option<Value>],
782) -> Result<String, String> {
783 match format {
784 "links-notation" => Ok(format_record_list_as_links_notation_with_current_times(
785 records,
786 current_times,
787 process_ids,
788 )),
789 "json" => serde_json::to_string_pretty(&record_list_json_with_current_times(
790 records,
791 current_times,
792 process_ids,
793 ))
794 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
795 "text" => Ok(format_record_list_as_text_with_current_times(
796 records,
797 current_times,
798 process_ids,
799 )),
800 _ => Err(format!("Unknown output format: {}", format)),
801 }
802}
803
804pub struct StatusQueryResult {
806 pub success: bool,
807 pub output: Option<String>,
808 pub error: Option<String>,
809}
810
811pub fn list_executions(
813 store: Option<&ExecutionStore>,
814 output_format: Option<&str>,
815) -> StatusQueryResult {
816 list_executions_filtered(store, output_format, false)
817}
818
819pub fn list_executions_filtered(
825 store: Option<&ExecutionStore>,
826 output_format: Option<&str>,
827 running_only: bool,
828) -> StatusQueryResult {
829 let store = match store {
830 Some(s) => s,
831 None => {
832 return StatusQueryResult {
833 success: false,
834 output: None,
835 error: Some("Execution tracking is disabled.".to_string()),
836 }
837 }
838 };
839
840 let mut records: Vec<ExecutionRecord> =
841 store.get_all().iter().map(enrich_detached_status).collect();
842 if running_only {
843 records.retain(|record| record.status == ExecutionStatus::Executing);
844 }
845 sort_records_by_start_time_desc(&mut records);
846 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
847 let process_ids: Vec<Option<Value>> = records.iter().map(collect_process_ids).collect();
848 let format = output_format.unwrap_or("links-notation");
849
850 match format_record_list_with_current_times(&records, format, ¤t_times, &process_ids) {
851 Ok(output) => StatusQueryResult {
852 success: true,
853 output: Some(output),
854 error: None,
855 },
856 Err(e) => StatusQueryResult {
857 success: false,
858 output: None,
859 error: Some(e),
860 },
861 }
862}
863
864pub fn query_status(
866 store: Option<&ExecutionStore>,
867 identifier: &str,
868 output_format: Option<&str>,
869) -> StatusQueryResult {
870 let store = match store {
871 Some(s) => s,
872 None => {
873 return StatusQueryResult {
874 success: false,
875 output: None,
876 error: Some("Execution tracking is disabled.".to_string()),
877 }
878 }
879 };
880
881 let record = match store.get(identifier) {
882 Some(r) => r,
883 None => {
884 return StatusQueryResult {
885 success: false,
886 output: None,
887 error: Some(format!(
888 "No execution found with UUID or session name: {}",
889 identifier
890 )),
891 }
892 }
893 };
894
895 let enriched = enrich_detached_status(&record);
897 crate::container_removal::persist_container_removal(store, &record, &enriched);
898 let current_time = attach_current_time(&enriched);
900 let process_ids = collect_process_ids(&enriched);
901
902 let format = output_format.unwrap_or("links-notation");
903 match format_record_with_enrichments(
904 &enriched,
905 format,
906 current_time.as_deref(),
907 process_ids.as_ref(),
908 ) {
909 Ok(output) => StatusQueryResult {
910 success: true,
911 output: Some(output),
912 error: None,
913 },
914 Err(e) => StatusQueryResult {
915 success: false,
916 output: None,
917 error: Some(e),
918 },
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::execution_store::ExecutionRecordOptions;
926 use serde_json::json;
927
928 fn executing_record() -> ExecutionRecord {
929 ExecutionRecord::with_options(ExecutionRecordOptions {
930 command: "sleep 60".to_string(),
931 uuid: Some("issue-126-rust".to_string()),
932 pid: Some(667105),
933 status: Some(ExecutionStatus::Executing),
934 log_path: Some("/tmp/issue-126.log".to_string()),
935 start_time: Some("2026-04-23T10:00:00Z".to_string()),
936 working_directory: Some("/home/user".to_string()),
937 shell: Some("/bin/bash".to_string()),
938 platform: Some("linux".to_string()),
939 ..Default::default()
940 })
941 }
942
943 #[test]
944 fn links_notation_indents_nested_process_id_arrays() {
945 let process_ids = json!({
946 "wrapperPid": 667105,
947 "screenPid": 667120,
948 "commandPids": [667121, 667122],
949 });
950 let output = format_record_with_enrichments(
951 &executing_record(),
952 "links-notation",
953 Some("2026-04-23T10:10:13.042Z"),
954 Some(&process_ids),
955 )
956 .expect("links-notation should format");
957
958 assert!(
959 output.contains(
960 " commandPids\n (\n 667121\n 667122\n )"
961 ),
962 "processIds should be a nested indented block, output: {}",
963 output
964 );
965 assert!(
966 !output.contains("\n(\n"),
967 "opening parenthesis must not start at column 1: {}",
968 output
969 );
970 }
971}