1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
use std::time::Duration;
use crate::errors::Result;
use crate::session::{Session, exec::exec};
use crate::tools::shell_quote;
pub struct TailChunk {
pub content: String,
pub bytes: usize,
pub exit_code: i32,
}
/// Read the last `lines` lines of `path`. If `follow=false` we just `tail -n`.
/// If `follow=true` we use `tail -F -n <lines>` capped at `max_wait`.
pub async fn tail(
session: &Session,
path: &str,
lines: u32,
follow: bool,
max_wait: Duration,
max_capture: usize,
) -> Result<TailChunk> {
let path_q = shell_quote(path);
let cmd = if follow {
// Piping through `head -c` makes the call return as soon as the
// capture cap is hit instead of sitting out the full `timeout` window
// (and shipping bytes that would be dropped anyway): head exits at the
// cap, tail dies on SIGPIPE, the pipeline completes immediately.
// `2>&1` routes tail warnings (missing file, rotation) into the
// captured output since the pipeline exit code is head's (0).
format!(
"timeout {sec} tail -n {lines} -F {path_q} 2>&1 | head -c {cap}",
sec = max_wait.as_secs().max(1),
cap = max_capture
)
} else {
format!("tail -n {lines} {path_q}")
};
let res = exec(
session,
&cmd,
max_wait + Duration::from_secs(if follow { 5 } else { 10 }),
max_capture,
)
.await?;
Ok(TailChunk {
content: res.stdout,
bytes: res.stdout_bytes,
exit_code: res.exit_code,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_go_through_the_shared_quoter() {
// This module used to carry its own escaper with a "looks safe, skip
// the quotes" fast path. One quoter, one behaviour.
assert_eq!(shell_quote("/var/log/syslog"), "'/var/log/syslog'");
assert_eq!(shell_quote("/tmp/has space.log"), "'/tmp/has space.log'");
assert_eq!(shell_quote("o'brien.log"), "'o'\\''brien.log'");
}
}