actr_cli/commands/
logs.rs1use crate::commands::runtime_state::{RuntimeStateStore, absolutize_from_cwd, resolve_hyper_dir};
2use crate::core::{Command, CommandContext, CommandResult, ComponentType};
3use crate::error::ActrCliError;
4use anyhow::Result;
5use async_trait::async_trait;
6use clap::Args;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10use tokio::io::{AsyncReadExt, AsyncSeekExt};
11
12#[derive(Args, Debug)]
13pub struct LogsCommand {
14 #[arg(value_name = "WID")]
16 pub wid: String,
17
18 #[arg(short = 'c', long = "config", value_name = "FILE")]
20 pub config: Option<PathBuf>,
21
22 #[arg(long = "hyper-dir", value_name = "DIR")]
24 pub hyper_dir: Option<PathBuf>,
25
26 #[arg(short = 'f', long = "follow")]
28 pub follow: bool,
29}
30
31#[async_trait]
32impl Command for LogsCommand {
33 async fn execute(&self, _ctx: &CommandContext) -> Result<CommandResult> {
34 let hyper_dir = resolve_hyper_dir(self.config.as_deref(), self.hyper_dir.as_deref())?;
35 let store = RuntimeStateStore::new(hyper_dir);
36 let entry = store.resolve_wid_prefix(&self.wid).await?;
37
38 let log_path = absolutize_log_path(&entry.record.log_path)?;
39 if !log_path.exists() {
40 return Err(ActrCliError::command_error(format!(
41 "Log file not found: {}",
42 log_path.display()
43 ))
44 .into());
45 }
46
47 stream_log_file(&log_path, self.follow).await?;
48 Ok(CommandResult::Success(String::new()))
49 }
50
51 fn required_components(&self) -> Vec<ComponentType> {
52 vec![]
53 }
54
55 fn name(&self) -> &str {
56 "logs"
57 }
58
59 fn description(&self) -> &str {
60 "Show logs for a detached runtime instance"
61 }
62}
63
64fn absolutize_log_path(path: &Path) -> crate::error::Result<PathBuf> {
65 if path.is_absolute() {
66 Ok(path.to_path_buf())
67 } else {
68 absolutize_from_cwd(path)
69 }
70}
71
72async fn stream_log_file(path: &Path, follow: bool) -> crate::error::Result<()> {
73 let mut file = tokio::fs::File::open(path).await.map_err(|error| {
74 ActrCliError::command_error(format!(
75 "Failed to open log file {}: {}",
76 path.display(),
77 error
78 ))
79 })?;
80 let mut offset = 0u64;
81 let mut stdout = std::io::stdout();
82
83 loop {
84 let metadata = file.metadata().await.map_err(|error| {
85 ActrCliError::command_error(format!(
86 "Failed to stat log file {}: {}",
87 path.display(),
88 error
89 ))
90 })?;
91 if metadata.len() < offset {
92 offset = 0;
93 }
94
95 file.seek(std::io::SeekFrom::Start(offset))
96 .await
97 .map_err(|error| {
98 ActrCliError::command_error(format!(
99 "Failed to seek log file {}: {}",
100 path.display(),
101 error
102 ))
103 })?;
104
105 let mut buffer = [0u8; 8192];
106 loop {
107 match file.read(&mut buffer).await {
108 Ok(0) => break,
109 Ok(n) => {
110 stdout.write_all(&buffer[..n]).map_err(ActrCliError::Io)?;
111 stdout.flush().map_err(ActrCliError::Io)?;
112 offset += n as u64;
113 }
114 Err(error) => {
115 return Err(ActrCliError::command_error(format!(
116 "Failed to read log file {}: {}",
117 path.display(),
118 error
119 )));
120 }
121 }
122 }
123
124 if !follow {
125 return Ok(());
126 }
127 tokio::time::sleep(Duration::from_millis(200)).await;
128 }
129}