Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ custom-protocol = ["tauri/custom-protocol"]
macos_installer = ["dep:dispatch2", "dep:objc2-system-extensions"]

[dev-dependencies]
tempfile.workspace = true
tokio = { workspace = true, features = ["full"] }
wiremock.workspace = true

Expand Down
1 change: 0 additions & 1 deletion src-tauri/client-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ url.workspace = true
webbrowser.workspace = true

[dev-dependencies]
sha1 = "0.11"
tempfile.workspace = true
tokio-stream.workspace = true
tonic.workspace = true
48 changes: 17 additions & 31 deletions src-tauri/src/log_watcher/global_log_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@

#[cfg(not(target_os = "macos"))]
use std::fs::read_dir;
use std::{
fs::File,
io::{BufRead, BufReader},
path::PathBuf,
str::FromStr,
time::Duration,
};
use std::{path::PathBuf, str::FromStr, time::Duration};

#[cfg(not(target_os = "macos"))]
use chrono::NaiveDate;
Expand All @@ -26,7 +20,9 @@ use crate::log_watcher::get_vpn_extension_log_dir_path;
use crate::{
appstate::AppState,
error::Error,
log_watcher::{LogLine, LogLineFields, LogSource, LogWatcherError},
log_watcher::{
log_file_reader::LogFileReader, LogLine, LogLineFields, LogSource, LogWatcherError,
},
LOG_FILENAME,
};
#[cfg(not(target_os = "macos"))]
Expand Down Expand Up @@ -125,11 +121,11 @@ impl LogDirs {
}

#[cfg(not(target_os = "macos"))]
fn get_current_service_file(&self) -> Result<File, LogWatcherError> {
fn get_current_service_file(&self) -> Result<LogFileReader, LogWatcherError> {
match &self.current_service_log_file {
Some(path) => {
trace!("Opening service log file: {}", path.display());
let file = File::open(path)?;
let file = LogFileReader::open(path)?;
trace!("Successfully opened service log file at {}", path.display());
Ok(file)
}
Expand All @@ -139,21 +135,21 @@ impl LogDirs {
}
}

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

/// Get the VPN extension log file (macOS only)
/// The VPN extension writes logs to the App Group shared container
#[cfg(target_os = "macos")]
fn get_vpn_extension_file(&self) -> Result<File, LogWatcherError> {
fn get_vpn_extension_file(&self) -> Result<LogFileReader, LogWatcherError> {
let path = self.vpn_extension_log_dir.join(VPN_EXTENSION_LOG_FILENAME);
trace!("Opening VPN extension log file: {}", path.display());
let file = File::open(&path)?;
let file = LogFileReader::open(&path)?;
trace!("VPN extension log file opened successfully");
Ok(file)
}
Expand Down Expand Up @@ -207,16 +203,8 @@ impl GlobalLogWatcher {
self.log_dirs.current_service_log_file
);

let mut service_reader = if let Ok(file) = self.log_dirs.get_current_service_file() {
Some(BufReader::new(file))
} else {
None
};
let mut client_reader = if let Ok(file) = self.log_dirs.get_client_file() {
Some(BufReader::new(file))
} else {
None
};
let mut service_reader = self.log_dirs.get_current_service_file().ok();
let mut client_reader = self.log_dirs.get_client_file().ok();

debug!("Checking if log files are available");
if service_reader.is_none() && client_reader.is_none() {
Expand Down Expand Up @@ -263,6 +251,7 @@ impl GlobalLogWatcher {
"Found a new service log file: {latest_log_file:?}, switching to it."
);
self.log_dirs.current_service_log_file = latest_log_file;
*reader = self.log_dirs.get_current_service_file()?;
break;
}
} else {
Expand Down Expand Up @@ -327,10 +316,7 @@ impl GlobalLogWatcher {
#[cfg(target_os = "macos")]
async fn parse_log_dirs(&self) -> Result<(), LogWatcherError> {
debug!("Processing log directories for client and VPN extension.");
let mut client_reader = self
.log_dirs
.get_client_file()
.map_or_else(|_| None, |file| Some(BufReader::new(file)));
let mut client_reader = self.log_dirs.get_client_file().ok();

let mut vpn_extension_reader = self.log_dirs.get_vpn_extension_file().map_or_else(
|_| {
Expand All @@ -339,7 +325,7 @@ impl GlobalLogWatcher {
},
|file| {
debug!("VPN extension log file opened successfully");
Some(BufReader::new(file))
Some(file)
},
);

Expand Down Expand Up @@ -394,7 +380,7 @@ impl GlobalLogWatcher {
// Try to open the client log file if it wasn't available before
if let Ok(file) = self.log_dirs.get_client_file() {
debug!("Client log file is now available, opening reader");
client_reader = Some(BufReader::new(file));
client_reader = Some(file);
}
}

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

Expand Down
154 changes: 154 additions & 0 deletions src-tauri/src/log_watcher/log_file_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! Line reader that follows a log file by path and reopens it after rotation.

#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(not(unix))]
use std::time::SystemTime;
use std::{
fs::{metadata, File, Metadata},
io::{self, BufRead, BufReader, Seek, SeekFrom},
path::{Path, PathBuf},
};

#[derive(PartialEq, Eq)]
struct FileId {
#[cfg(unix)]
dev: u64,
#[cfg(unix)]
ino: u64,
#[cfg(not(unix))]
created: Option<SystemTime>,
}

impl FileId {
fn new(meta: &Metadata) -> Self {
#[cfg(unix)]
{
Self {
dev: meta.dev(),
ino: meta.ino(),
}
}
#[cfg(not(unix))]
{
Self {
created: meta.created().ok(),
}
}
}
}

pub(crate) struct LogFileReader {
path: PathBuf,
reader: BufReader<File>,
id: FileId,
}

impl LogFileReader {
pub(crate) fn open(path: &Path) -> io::Result<Self> {
let file = File::open(path)?;
let id = FileId::new(&file.metadata()?);
Ok(Self {
path: path.to_path_buf(),
reader: BufReader::new(file),
id,
})
}

/// Like [`BufRead::read_line`], but on EOF switches to a rotated or truncated file.
pub(crate) fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
let size = self.reader.read_line(buf)?;
if size == 0 && self.reopen_if_rotated()? {
self.reader.read_line(buf)
} else {
Ok(size)
}
}

fn reopen_if_rotated(&mut self) -> io::Result<bool> {
let meta = match metadata(&self.path) {
Ok(meta) => meta,
// Mid-rotation; keep the old file for now.
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(err) => return Err(err),
};
let position = self.reader.stream_position()?;
if FileId::new(&meta) != self.id {
debug!("Log file {} was rotated, reopening", self.path.display());
*self = Self::open(&self.path)?;
Ok(true)
} else if meta.len() < position {
debug!("Log file {} was truncated, rewinding", self.path.display());
self.reader.seek(SeekFrom::Start(0))?;
Ok(true)
} else {
Ok(false)
}
}
}

#[cfg(test)]
mod tests {
use std::{fs::rename, io::Write};

use super::*;

fn append(path: &Path, text: &str) {
let mut file = File::options()
.create(true)
.append(true)
.open(path)
.unwrap();
file.write_all(text.as_bytes()).unwrap();
}

fn read_all(reader: &mut LogFileReader) -> Vec<String> {
let mut lines = Vec::new();
let mut line = String::new();
while reader.read_line(&mut line).unwrap() > 0 {
lines.push(line.clone());
line.clear();
}
lines
}

#[test]
fn follows_appends() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
append(&path, "one\n");
let mut reader = LogFileReader::open(&path).unwrap();
assert_eq!(read_all(&mut reader), ["one\n"]);
append(&path, "two\n");
assert_eq!(read_all(&mut reader), ["two\n"]);
}

#[test]
fn follows_rename_rotation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
append(&path, "one\n");
let mut reader = LogFileReader::open(&path).unwrap();
assert_eq!(read_all(&mut reader), ["one\n"]);

append(&path, "two\n");
rename(&path, dir.path().join("test.1.log")).unwrap();
assert_eq!(read_all(&mut reader), ["two\n"]);

append(&path, "three\n");
assert_eq!(read_all(&mut reader), ["three\n"]);
}

#[test]
fn follows_truncation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
append(&path, "one two three\n");
let mut reader = LogFileReader::open(&path).unwrap();
assert_eq!(read_all(&mut reader), ["one two three\n"]);

File::create(&path).unwrap();
append(&path, "four\n");
assert_eq!(read_all(&mut reader), ["four\n"]);
}
}
1 change: 1 addition & 0 deletions src-tauri/src/log_watcher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const SERVICE_LOG_PREFIX: &str = "defguard-service.";
const SERVICE_LOG_SUFFIX: &str = ".log";

pub mod global_log_watcher;
mod log_file_reader;
pub mod service_log_watcher;

#[derive(Error, Debug)]
Expand Down
5 changes: 2 additions & 3 deletions src-tauri/src/log_watcher/service_log_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use tokio_util::sync::CancellationToken;
use tracing::Level;

#[cfg(target_os = "macos")]
use super::LogLineFields;
use super::{log_file_reader::LogFileReader, LogLineFields};
use super::{LogLine, LogWatcherError};
#[cfg(not(target_os = "macos"))]
use crate::utils::DEFAULT_SERVICE_LOG_DIR;
Expand Down Expand Up @@ -265,8 +265,7 @@ impl VpnExtensionLogWatcher {
sleep(DELAY);
}

let file = File::open(&self.log_file)?;
let mut reader = BufReader::new(file);
let mut reader = LogFileReader::open(&self.log_file)?;
let mut line = String::new();
let mut parsed_lines = Vec::new();

Expand Down
9 changes: 5 additions & 4 deletions src-tauri/src/window_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,14 @@ pub async fn initiate_configure_factor_screen(
// A location that went away in the meantime is not worth refusing the screen over, it just
// loses the steps it would have spoken to.
let location = match location_id {
Some(location_id) => match Location::find_by_id(&*DB_POOL, location_id).await? {
Some(location) => Some(build_location_info(location, &connected_location_ids)),
None => {
Some(location_id) => {
if let Some(location) = Location::find_by_id(&*DB_POOL, location_id).await? {
Some(build_location_info(location, &connected_location_ids))
} else {
warn!("Configure factors requested from unknown location {location_id}");
None
}
},
}
None => None,
};

Expand Down
Loading