audb_core/features/
logs.rs

1// Logs command implementation for Aurora OS devices
2//
3// Retrieve device logs via journalctl with Android/iOS-like interface.
4
5use crate::features::config::{device_store::DeviceStore, state::DeviceState};
6use crate::tools::{
7    macros::{print_error, print_info},
8    session::DeviceSession,
9    shell_escape::escape_single_quote,
10    types::DeviceIdentifier,
11};
12use anyhow::{anyhow, Context, Result};
13
14pub struct LogsArgs {
15    pub lines: usize,
16    pub priority: Option<crate::LogLevel>,
17    pub unit: Option<String>,
18    pub grep: Option<String>,
19    pub since: Option<String>,
20    pub clear: bool,
21    pub force: bool,
22    pub kernel: bool,
23}
24
25pub async fn execute(args: LogsArgs) -> Result<()> {
26    if args.clear {
27        return execute_clear_logs(args.force).await;
28    }
29
30    validate_args(&args)?;
31
32    // Get device and establish session
33    let current_host = DeviceState::get_current()?;
34    let device_id = DeviceIdentifier::Host(current_host);
35    let device = DeviceStore::find(&device_id)?;
36
37    let mut session = DeviceSession::connect(&device)
38        .context("Failed to connect to device")?;
39
40    // Build and execute journalctl command
41    let command = build_journalctl_command(&args)?;
42    print_info(format!("Retrieving logs from {}...", device.display_name()));
43
44    let output = session.exec_as_root(&command)
45        .context("Failed to retrieve logs. Root access required.")?;
46
47    // Print logs
48    for line in &output {
49        println!("{}", line);
50    }
51
52    if output.is_empty() {
53        print_info("No logs found matching the criteria");
54    }
55
56    Ok(())
57}
58
59async fn execute_clear_logs(force: bool) -> Result<()> {
60    // Require --force flag to prevent accidents
61    if !force {
62        return Err(anyhow!(
63            "Clearing logs requires --force flag. Use: audb logs --clear --force"
64        ));
65    }
66
67    // Get device and establish session
68    let current_host = DeviceState::get_current()?;
69    let device_id = DeviceIdentifier::Host(current_host);
70    let device = DeviceStore::find(&device_id)?;
71
72    let mut session = DeviceSession::connect(&device)
73        .context("Failed to connect to device")?;
74
75    print_info(format!(
76        "Clearing logs on {}...",
77        device.display_name()
78    ));
79
80    let command = "journalctl --rotate && journalctl --vacuum-time=1s";
81    session
82        .exec_as_root(command)
83        .context("Failed to clear logs. Root access required.")?;
84
85    print_info("Logs cleared successfully");
86    Ok(())
87}
88
89fn validate_args(args: &LogsArgs) -> Result<()> {
90    // Validate lines count
91    if args.lines == 0 {
92        return Err(anyhow!("Lines count must be greater than 0"));
93    }
94    if args.lines > 10000 {
95        print_error("Large line count may take time to retrieve");
96    }
97
98    // Validate that kernel and unit are mutually exclusive
99    if args.kernel && args.unit.is_some() {
100        return Err(anyhow!(
101            "Cannot specify both --kernel and --unit"
102        ));
103    }
104
105    Ok(())
106}
107
108fn build_journalctl_command(args: &LogsArgs) -> Result<String> {
109    let mut cmd = String::from("journalctl");
110
111    // Kernel messages mode
112    if args.kernel {
113        cmd.push_str(" -k");
114    }
115
116    // Number of lines
117    cmd.push_str(&format!(" -n {}", args.lines));
118
119    // Priority level
120    if let Some(ref priority) = args.priority {
121        cmd.push_str(&format!(" -p {}", priority.to_journalctl_priority()));
122    }
123
124    // Unit filter (with shell escaping)
125    if let Some(ref unit) = args.unit {
126        let escaped = escape_single_quote(unit);
127        cmd.push_str(&format!(" -u '{}'", escaped));
128    }
129
130    // Time filter (with shell escaping)
131    if let Some(ref since) = args.since {
132        let escaped = escape_single_quote(since);
133        cmd.push_str(&format!(" --since '{}'", escaped));
134    }
135
136    // Output options
137    cmd.push_str(" --no-pager --no-hostname");
138
139    // Grep filter (as pipe, with escaping)
140    if let Some(ref grep_pattern) = args.grep {
141        let escaped = escape_single_quote(grep_pattern);
142        cmd.push_str(&format!(" | grep '{}'", escaped));
143    }
144
145    Ok(cmd)
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_build_basic_command() {
154        let args = LogsArgs {
155            lines: 100,
156            priority: None,
157            unit: None,
158            since: None,
159            grep: None,
160            clear: false,
161            force: false,
162            kernel: false,
163        };
164
165        let cmd = build_journalctl_command(&args).unwrap();
166        assert!(cmd.contains("journalctl"));
167        assert!(cmd.contains("-n 100"));
168        assert!(cmd.contains("--no-pager"));
169        assert!(cmd.contains("--no-hostname"));
170    }
171
172    #[test]
173    fn test_build_command_with_priority() {
174        let args = LogsArgs {
175            lines: 50,
176            priority: Some(crate::LogLevel::Err),
177            unit: None,
178            since: None,
179            grep: None,
180            clear: false,
181            force: false,
182            kernel: false,
183        };
184
185        let cmd = build_journalctl_command(&args).unwrap();
186        assert!(cmd.contains("-p err"));
187    }
188
189    #[test]
190    fn test_build_command_with_unit() {
191        let args = LogsArgs {
192            lines: 100,
193            priority: None,
194            unit: Some("test.service".to_string()),
195            since: None,
196            grep: None,
197            clear: false,
198            force: false,
199            kernel: false,
200        };
201
202        let cmd = build_journalctl_command(&args).unwrap();
203        assert!(cmd.contains("-u 'test.service'"));
204    }
205
206    #[test]
207    fn test_build_command_with_kernel() {
208        let args = LogsArgs {
209            lines: 100,
210            priority: None,
211            unit: None,
212            since: None,
213            grep: None,
214            clear: false,
215            force: false,
216            kernel: true,
217        };
218
219        let cmd = build_journalctl_command(&args).unwrap();
220        assert!(cmd.contains(" -k"));
221    }
222
223    #[test]
224    fn test_build_command_with_grep() {
225        let args = LogsArgs {
226            lines: 100,
227            priority: None,
228            unit: None,
229            since: None,
230            grep: Some("ERROR".to_string()),
231            clear: false,
232            force: false,
233            kernel: false,
234        };
235
236        let cmd = build_journalctl_command(&args).unwrap();
237        assert!(cmd.contains("| grep 'ERROR'"));
238    }
239
240    #[test]
241    fn test_shell_injection_protection() {
242        let args = LogsArgs {
243            lines: 100,
244            priority: None,
245            unit: Some("evil'; rm -rf /; echo '".to_string()),
246            since: None,
247            grep: None,
248            clear: false,
249            force: false,
250            kernel: false,
251        };
252
253        let cmd = build_journalctl_command(&args).unwrap();
254        // Should escape single quotes
255        assert!(cmd.contains("'\\''"));
256    }
257
258    #[test]
259    fn test_validate_kernel_unit_conflict() {
260        let args = LogsArgs {
261            lines: 100,
262            priority: None,
263            unit: Some("test.service".to_string()),
264            since: None,
265            grep: None,
266            clear: false,
267            force: false,
268            kernel: true,
269        };
270
271        assert!(validate_args(&args).is_err());
272    }
273
274    #[test]
275    fn test_validate_zero_lines() {
276        let args = LogsArgs {
277            lines: 0,
278            priority: None,
279            unit: None,
280            since: None,
281            grep: None,
282            clear: false,
283            force: false,
284            kernel: false,
285        };
286
287        assert!(validate_args(&args).is_err());
288    }
289
290    #[test]
291    fn test_validate_valid_args() {
292        let args = LogsArgs {
293            lines: 100,
294            priority: None,
295            unit: None,
296            since: None,
297            grep: None,
298            clear: false,
299            force: false,
300            kernel: false,
301        };
302
303        assert!(validate_args(&args).is_ok());
304    }
305}