1use 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 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 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 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 if !force {
62 return Err(anyhow!(
63 "Clearing logs requires --force flag. Use: audb logs --clear --force"
64 ));
65 }
66
67 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 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 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 if args.kernel {
113 cmd.push_str(" -k");
114 }
115
116 cmd.push_str(&format!(" -n {}", args.lines));
118
119 if let Some(ref priority) = args.priority {
121 cmd.push_str(&format!(" -p {}", priority.to_journalctl_priority()));
122 }
123
124 if let Some(ref unit) = args.unit {
126 let escaped = escape_single_quote(unit);
127 cmd.push_str(&format!(" -u '{}'", escaped));
128 }
129
130 if let Some(ref since) = args.since {
132 let escaped = escape_single_quote(since);
133 cmd.push_str(&format!(" --since '{}'", escaped));
134 }
135
136 cmd.push_str(" --no-pager --no-hostname");
138
139 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 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}