Skip to content

Commit 75dd39b

Browse files
committed
Handle log rotation in log watcher
1 parent 5a5e0d6 commit 75dd39b

7 files changed

Lines changed: 175 additions & 31 deletions

File tree

‎src-tauri/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ custom-protocol = ["tauri/custom-protocol"]
207207
macos_installer = ["dep:dispatch2", "dep:objc2-system-extensions"]
208208

209209
[dev-dependencies]
210+
tempfile.workspace = true
210211
tokio = { workspace = true, features = ["full"] }
211212
wiremock.workspace = true
212213

‎src-tauri/client-cli/Cargo.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ url.workspace = true
3333
webbrowser.workspace = true
3434

3535
[dev-dependencies]
36-
sha1 = "0.11"
3736
tempfile.workspace = true
3837
tokio-stream.workspace = true
3938
tonic.workspace = true

‎src-tauri/src/log_watcher/global_log_watcher.rs‎

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
#[cfg(not(target_os = "macos"))]
66
use std::fs::read_dir;
77
use std::{
8-
fs::File,
9-
io::{BufRead, BufReader},
108
path::PathBuf,
119
str::FromStr,
1210
time::Duration,
@@ -26,7 +24,9 @@ use crate::log_watcher::get_vpn_extension_log_dir_path;
2624
use crate::{
2725
appstate::AppState,
2826
error::Error,
29-
log_watcher::{LogLine, LogLineFields, LogSource, LogWatcherError},
27+
log_watcher::{
28+
log_file_reader::LogFileReader, LogLine, LogLineFields, LogSource, LogWatcherError,
29+
},
3030
LOG_FILENAME,
3131
};
3232
#[cfg(not(target_os = "macos"))]
@@ -125,11 +125,11 @@ impl LogDirs {
125125
}
126126

127127
#[cfg(not(target_os = "macos"))]
128-
fn get_current_service_file(&self) -> Result<File, LogWatcherError> {
128+
fn get_current_service_file(&self) -> Result<LogFileReader, LogWatcherError> {
129129
match &self.current_service_log_file {
130130
Some(path) => {
131131
trace!("Opening service log file: {}", path.display());
132-
let file = File::open(path)?;
132+
let file = LogFileReader::open(path)?;
133133
trace!("Successfully opened service log file at {}", path.display());
134134
Ok(file)
135135
}
@@ -139,21 +139,21 @@ impl LogDirs {
139139
}
140140
}
141141

142-
fn get_client_file(&self) -> Result<File, LogWatcherError> {
142+
fn get_client_file(&self) -> Result<LogFileReader, LogWatcherError> {
143143
let path = self.client_log_dir.join(format!("{LOG_FILENAME}.log"));
144144
trace!("Constructed client log file path: {}", path.display());
145-
let file = File::open(&path)?;
145+
let file = LogFileReader::open(&path)?;
146146
trace!("Client log file at {} opened successfully", path.display());
147147
Ok(file)
148148
}
149149

150150
/// Get the VPN extension log file (macOS only)
151151
/// The VPN extension writes logs to the App Group shared container
152152
#[cfg(target_os = "macos")]
153-
fn get_vpn_extension_file(&self) -> Result<File, LogWatcherError> {
153+
fn get_vpn_extension_file(&self) -> Result<LogFileReader, LogWatcherError> {
154154
let path = self.vpn_extension_log_dir.join(VPN_EXTENSION_LOG_FILENAME);
155155
trace!("Opening VPN extension log file: {}", path.display());
156-
let file = File::open(&path)?;
156+
let file = LogFileReader::open(&path)?;
157157
trace!("VPN extension log file opened successfully");
158158
Ok(file)
159159
}
@@ -207,16 +207,8 @@ impl GlobalLogWatcher {
207207
self.log_dirs.current_service_log_file
208208
);
209209

210-
let mut service_reader = if let Ok(file) = self.log_dirs.get_current_service_file() {
211-
Some(BufReader::new(file))
212-
} else {
213-
None
214-
};
215-
let mut client_reader = if let Ok(file) = self.log_dirs.get_client_file() {
216-
Some(BufReader::new(file))
217-
} else {
218-
None
219-
};
210+
let mut service_reader = self.log_dirs.get_current_service_file().ok();
211+
let mut client_reader = self.log_dirs.get_client_file().ok();
220212

221213
debug!("Checking if log files are available");
222214
if service_reader.is_none() && client_reader.is_none() {
@@ -263,6 +255,7 @@ impl GlobalLogWatcher {
263255
"Found a new service log file: {latest_log_file:?}, switching to it."
264256
);
265257
self.log_dirs.current_service_log_file = latest_log_file;
258+
*reader = self.log_dirs.get_current_service_file()?;
266259
break;
267260
}
268261
} else {
@@ -327,10 +320,7 @@ impl GlobalLogWatcher {
327320
#[cfg(target_os = "macos")]
328321
async fn parse_log_dirs(&self) -> Result<(), LogWatcherError> {
329322
debug!("Processing log directories for client and VPN extension.");
330-
let mut client_reader = self
331-
.log_dirs
332-
.get_client_file()
333-
.map_or_else(|_| None, |file| Some(BufReader::new(file)));
323+
let mut client_reader = self.log_dirs.get_client_file().ok();
334324

335325
let mut vpn_extension_reader = self.log_dirs.get_vpn_extension_file().map_or_else(
336326
|_| {
@@ -339,7 +329,7 @@ impl GlobalLogWatcher {
339329
},
340330
|file| {
341331
debug!("VPN extension log file opened successfully");
342-
Some(BufReader::new(file))
332+
Some(file)
343333
},
344334
);
345335

@@ -394,7 +384,7 @@ impl GlobalLogWatcher {
394384
// Try to open the client log file if it wasn't available before
395385
if let Ok(file) = self.log_dirs.get_client_file() {
396386
debug!("Client log file is now available, opening reader");
397-
client_reader = Some(BufReader::new(file));
387+
client_reader = Some(file);
398388
}
399389
}
400390

@@ -420,7 +410,7 @@ impl GlobalLogWatcher {
420410
// Try to open the VPN extension log file if it wasn't available before
421411
if let Ok(file) = self.log_dirs.get_vpn_extension_file() {
422412
debug!("VPN extension log file is now available, opening reader");
423-
vpn_extension_reader = Some(BufReader::new(file));
413+
vpn_extension_reader = Some(file);
424414
}
425415
}
426416

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//! Line reader that follows a log file by path and reopens it after rotation.
2+
3+
#[cfg(unix)]
4+
use std::os::unix::fs::MetadataExt;
5+
#[cfg(not(unix))]
6+
use std::time::SystemTime;
7+
use std::{
8+
fs::{metadata, File, Metadata},
9+
io::{self, BufRead, BufReader, Seek, SeekFrom},
10+
path::{Path, PathBuf},
11+
};
12+
13+
#[derive(PartialEq, Eq)]
14+
struct FileId {
15+
#[cfg(unix)]
16+
dev: u64,
17+
#[cfg(unix)]
18+
ino: u64,
19+
#[cfg(not(unix))]
20+
created: Option<SystemTime>,
21+
}
22+
23+
impl FileId {
24+
fn new(meta: &Metadata) -> Self {
25+
#[cfg(unix)]
26+
{
27+
Self {
28+
dev: meta.dev(),
29+
ino: meta.ino(),
30+
}
31+
}
32+
#[cfg(not(unix))]
33+
{
34+
Self {
35+
created: meta.created().ok(),
36+
}
37+
}
38+
}
39+
}
40+
41+
pub(crate) struct LogFileReader {
42+
path: PathBuf,
43+
reader: BufReader<File>,
44+
id: FileId,
45+
}
46+
47+
impl LogFileReader {
48+
pub(crate) fn open(path: &Path) -> io::Result<Self> {
49+
let file = File::open(path)?;
50+
let id = FileId::new(&file.metadata()?);
51+
Ok(Self {
52+
path: path.to_path_buf(),
53+
reader: BufReader::new(file),
54+
id,
55+
})
56+
}
57+
58+
/// Like [`BufRead::read_line`], but on EOF switches to a rotated or truncated file.
59+
pub(crate) fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
60+
let size = self.reader.read_line(buf)?;
61+
if size == 0 && self.reopen_if_rotated()? {
62+
self.reader.read_line(buf)
63+
} else {
64+
Ok(size)
65+
}
66+
}
67+
68+
fn reopen_if_rotated(&mut self) -> io::Result<bool> {
69+
let meta = match metadata(&self.path) {
70+
Ok(meta) => meta,
71+
// Mid-rotation; keep the old file for now.
72+
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
73+
Err(err) => return Err(err),
74+
};
75+
let position = self.reader.stream_position()?;
76+
if FileId::new(&meta) != self.id {
77+
debug!("Log file {} was rotated, reopening", self.path.display());
78+
*self = Self::open(&self.path)?;
79+
Ok(true)
80+
} else if meta.len() < position {
81+
debug!("Log file {} was truncated, rewinding", self.path.display());
82+
self.reader.seek(SeekFrom::Start(0))?;
83+
Ok(true)
84+
} else {
85+
Ok(false)
86+
}
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use std::{fs::rename, io::Write};
93+
94+
use super::*;
95+
96+
fn append(path: &Path, text: &str) {
97+
let mut file = File::options()
98+
.create(true)
99+
.append(true)
100+
.open(path)
101+
.unwrap();
102+
file.write_all(text.as_bytes()).unwrap();
103+
}
104+
105+
fn read_all(reader: &mut LogFileReader) -> Vec<String> {
106+
let mut lines = Vec::new();
107+
let mut line = String::new();
108+
while reader.read_line(&mut line).unwrap() > 0 {
109+
lines.push(line.clone());
110+
line.clear();
111+
}
112+
lines
113+
}
114+
115+
#[test]
116+
fn follows_appends() {
117+
let dir = tempfile::tempdir().unwrap();
118+
let path = dir.path().join("test.log");
119+
append(&path, "one\n");
120+
let mut reader = LogFileReader::open(&path).unwrap();
121+
assert_eq!(read_all(&mut reader), ["one\n"]);
122+
append(&path, "two\n");
123+
assert_eq!(read_all(&mut reader), ["two\n"]);
124+
}
125+
126+
#[test]
127+
fn follows_rename_rotation() {
128+
let dir = tempfile::tempdir().unwrap();
129+
let path = dir.path().join("test.log");
130+
append(&path, "one\n");
131+
let mut reader = LogFileReader::open(&path).unwrap();
132+
assert_eq!(read_all(&mut reader), ["one\n"]);
133+
134+
append(&path, "two\n");
135+
rename(&path, dir.path().join("test.1.log")).unwrap();
136+
assert_eq!(read_all(&mut reader), ["two\n"]);
137+
138+
append(&path, "three\n");
139+
assert_eq!(read_all(&mut reader), ["three\n"]);
140+
}
141+
142+
#[test]
143+
fn follows_truncation() {
144+
let dir = tempfile::tempdir().unwrap();
145+
let path = dir.path().join("test.log");
146+
append(&path, "one two three\n");
147+
let mut reader = LogFileReader::open(&path).unwrap();
148+
assert_eq!(read_all(&mut reader), ["one two three\n"]);
149+
150+
File::create(&path).unwrap();
151+
append(&path, "four\n");
152+
assert_eq!(read_all(&mut reader), ["four\n"]);
153+
}
154+
}

‎src-tauri/src/log_watcher/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const SERVICE_LOG_PREFIX: &str = "defguard-service.";
1313
const SERVICE_LOG_SUFFIX: &str = ".log";
1414

1515
pub mod global_log_watcher;
16+
mod log_file_reader;
1617
pub mod service_log_watcher;
1718

1819
#[derive(Error, Debug)]

‎src-tauri/src/log_watcher/service_log_watcher.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use tokio_util::sync::CancellationToken;
2424
use tracing::Level;
2525

2626
#[cfg(target_os = "macos")]
27-
use super::LogLineFields;
27+
use super::{log_file_reader::LogFileReader, LogLineFields};
2828
use super::{LogLine, LogWatcherError};
2929
#[cfg(not(target_os = "macos"))]
3030
use crate::utils::DEFAULT_SERVICE_LOG_DIR;
@@ -265,8 +265,7 @@ impl VpnExtensionLogWatcher {
265265
sleep(DELAY);
266266
}
267267

268-
let file = File::open(&self.log_file)?;
269-
let mut reader = BufReader::new(file);
268+
let mut reader = LogFileReader::open(&self.log_file)?;
270269
let mut line = String::new();
271270
let mut parsed_lines = Vec::new();
272271

0 commit comments

Comments
 (0)