Skip to main content

actr_cli/commands/
logs.rs

1use 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    /// WID (or unique prefix, min 8 chars) of the runtime
15    #[arg(value_name = "WID")]
16    pub wid: String,
17
18    /// Runtime configuration file
19    #[arg(short = 'c', long = "config", value_name = "FILE")]
20    pub config: Option<PathBuf>,
21
22    /// Hyper data directory
23    #[arg(long = "hyper-dir", value_name = "DIR")]
24    pub hyper_dir: Option<PathBuf>,
25
26    /// Follow appended log output
27    #[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}