mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-20 06:28:19 +02:00
631 lines
18 KiB
Rust
631 lines
18 KiB
Rust
use crate::proxy_storage::{
|
|
delete_proxy_config, generate_proxy_id, get_proxy_config, is_process_running, list_proxy_configs,
|
|
save_proxy_config, ProxyConfig,
|
|
};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Stdio;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
static PROXY_PROCESSES: std::sync::LazyLock<
|
|
std::sync::Mutex<std::collections::HashMap<String, u32>>,
|
|
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
|
|
|
|
static SIDECAR_VERSION_VERIFIED: AtomicBool = AtomicBool::new(false);
|
|
const RETAINED_PROXY_LOGS: usize = 20;
|
|
|
|
fn prune_stale_proxy_logs(temp_dir: &Path, retain: usize) {
|
|
let active_ids = PROXY_PROCESSES
|
|
.lock()
|
|
.map(|processes| processes.keys().cloned().collect::<Vec<_>>())
|
|
.unwrap_or_default();
|
|
let Ok(entries) = std::fs::read_dir(temp_dir) else {
|
|
return;
|
|
};
|
|
let mut logs = entries
|
|
.flatten()
|
|
.filter_map(|entry| {
|
|
let file_name = entry.file_name();
|
|
let file_name = file_name.to_str()?;
|
|
let id = file_name
|
|
.strip_prefix("donut-proxy-")?
|
|
.strip_suffix(".log")?;
|
|
if active_ids.iter().any(|active_id| active_id == id) {
|
|
return None;
|
|
}
|
|
let modified = entry
|
|
.metadata()
|
|
.and_then(|metadata| metadata.modified())
|
|
.unwrap_or(std::time::UNIX_EPOCH);
|
|
Some((modified, entry.path()))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
logs.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.0));
|
|
for (_, path) in logs.into_iter().skip(retain) {
|
|
if let Err(error) = std::fs::remove_file(&path) {
|
|
log::debug!(
|
|
"Failed to prune stale proxy log {}: {error}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn target_binary_name(base_name: &str) -> String {
|
|
let target = env!("DONUT_BUILD_TARGET");
|
|
#[cfg(windows)]
|
|
{
|
|
format!("{base_name}-{target}.exe")
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
format!("{base_name}-{target}")
|
|
}
|
|
}
|
|
|
|
fn unsuffixed_binary_name(base_name: &str) -> String {
|
|
#[cfg(windows)]
|
|
{
|
|
format!("{base_name}.exe")
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
base_name.to_string()
|
|
}
|
|
}
|
|
|
|
fn is_executable_file(path: &Path) -> bool {
|
|
let Ok(metadata) = path.metadata() else {
|
|
return false;
|
|
};
|
|
if !metadata.is_file() {
|
|
return false;
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
metadata.permissions().mode() & 0o111 != 0
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
path
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.is_some_and(|extension| extension.eq_ignore_ascii_case("exe"))
|
|
}
|
|
}
|
|
|
|
fn push_candidate_dir(dirs: &mut Vec<PathBuf>, dir: Option<PathBuf>) {
|
|
if let Some(dir) = dir {
|
|
if !dirs.iter().any(|existing| existing == &dir) {
|
|
dirs.push(dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn find_sidecar_executable(
|
|
base_name: &str,
|
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
let current_exe = std::env::current_exe()?;
|
|
let current_dir = current_exe
|
|
.parent()
|
|
.ok_or("Failed to get parent directory of current executable")?;
|
|
|
|
if current_exe
|
|
.file_stem()
|
|
.and_then(|stem| stem.to_str())
|
|
.is_some_and(|stem| stem == base_name)
|
|
{
|
|
return Ok(current_exe);
|
|
}
|
|
|
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let mut search_dirs = Vec::new();
|
|
|
|
push_candidate_dir(&mut search_dirs, Some(current_dir.to_path_buf()));
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
current_dir.parent().map(std::path::Path::to_path_buf),
|
|
);
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
current_dir
|
|
.parent()
|
|
.and_then(|parent| parent.parent())
|
|
.map(Path::to_path_buf),
|
|
);
|
|
push_candidate_dir(&mut search_dirs, Some(current_dir.join("binaries")));
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
current_dir.parent().map(|parent| parent.join("binaries")),
|
|
);
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
current_dir
|
|
.parent()
|
|
.and_then(|parent| parent.parent())
|
|
.map(|parent| parent.join("binaries")),
|
|
);
|
|
push_candidate_dir(&mut search_dirs, Some(manifest_dir.join("binaries")));
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
Some(manifest_dir.join("target").join("debug")),
|
|
);
|
|
push_candidate_dir(
|
|
&mut search_dirs,
|
|
Some(manifest_dir.join("target").join("release")),
|
|
);
|
|
|
|
let exact_names = [
|
|
unsuffixed_binary_name(base_name),
|
|
target_binary_name(base_name),
|
|
];
|
|
|
|
for dir in &search_dirs {
|
|
for name in &exact_names {
|
|
if name.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let candidate = dir.join(name);
|
|
if is_executable_file(&candidate) {
|
|
return Ok(candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(
|
|
format!(
|
|
"Failed to locate '{}' executable. Searched in: {}",
|
|
base_name,
|
|
search_dirs
|
|
.iter()
|
|
.map(|dir| dir.display().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
)
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn parse_sidecar_version(stdout: &[u8]) -> Option<String> {
|
|
let output = std::str::from_utf8(stdout).ok()?.trim();
|
|
output
|
|
.strip_prefix("donut-proxy ")
|
|
.map(str::trim)
|
|
.filter(|version| !version.is_empty() && !version.contains(char::is_whitespace))
|
|
.map(str::to_string)
|
|
}
|
|
|
|
fn sidecar_version_mismatch_error() -> Box<dyn std::error::Error> {
|
|
serde_json::json!({
|
|
"code": "PROXY_SIDECAR_VERSION_MISMATCH"
|
|
})
|
|
.to_string()
|
|
.into()
|
|
}
|
|
|
|
/// Verify that the installed sidecar was built for the same release as the
|
|
/// main app. Windows can otherwise retain an executing, locked sidecar while
|
|
/// NSIS replaces the app, leaving an incompatible mixed-version installation.
|
|
pub(crate) async fn ensure_sidecar_version() -> Result<(), Box<dyn std::error::Error>> {
|
|
if SIDECAR_VERSION_VERIFIED.load(Ordering::Acquire) {
|
|
return Ok(());
|
|
}
|
|
|
|
let executable = match find_sidecar_executable("donut-proxy") {
|
|
Ok(executable) => executable,
|
|
Err(e) => {
|
|
log::error!("Failed to locate donut-proxy for version verification: {e}");
|
|
return Err(sidecar_version_mismatch_error());
|
|
}
|
|
};
|
|
let mut command = std::process::Command::new(&executable);
|
|
command.arg("--version");
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
command.creation_flags(CREATE_NO_WINDOW);
|
|
}
|
|
|
|
let output = match command.output() {
|
|
Ok(output) => output,
|
|
Err(e) => {
|
|
log::error!(
|
|
"Failed to run {} for version verification: {e}",
|
|
executable.display()
|
|
);
|
|
return Err(sidecar_version_mismatch_error());
|
|
}
|
|
};
|
|
let actual_version = parse_sidecar_version(&output.stdout);
|
|
let expected_version = env!("BUILD_VERSION");
|
|
|
|
if output.status.success() && actual_version.as_deref() == Some(expected_version) {
|
|
SIDECAR_VERSION_VERIFIED.store(true, Ordering::Release);
|
|
return Ok(());
|
|
}
|
|
|
|
log::error!(
|
|
"donut-proxy version mismatch: expected {}, got {:?}; status={}, stdout={:?}, stderr={:?}",
|
|
expected_version,
|
|
actual_version,
|
|
output.status,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
Err(sidecar_version_mismatch_error())
|
|
}
|
|
|
|
pub async fn start_proxy_process(
|
|
upstream_url: Option<String>,
|
|
port: Option<u16>,
|
|
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
|
|
start_proxy_process_with_profile(
|
|
upstream_url,
|
|
port,
|
|
None,
|
|
Vec::new(),
|
|
None,
|
|
false,
|
|
None,
|
|
true,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn start_proxy_process_with_profile(
|
|
upstream_url: Option<String>,
|
|
port: Option<u16>,
|
|
profile_id: Option<String>,
|
|
bypass_rules: Vec<String>,
|
|
blocklist_file: Option<String>,
|
|
dns_allowlist_mode: bool,
|
|
local_protocol: Option<String>,
|
|
record_domains: bool,
|
|
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
|
|
ensure_sidecar_version().await?;
|
|
|
|
let id = generate_proxy_id();
|
|
let upstream = upstream_url.unwrap_or_else(|| "DIRECT".to_string());
|
|
|
|
// Get available port if not specified
|
|
let local_port = port.unwrap_or_else(|| {
|
|
// Find an available port
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
|
listener.local_addr().unwrap().port()
|
|
});
|
|
|
|
let config = ProxyConfig::new(id.clone(), upstream, Some(local_port))
|
|
.with_profile_id(profile_id.clone())
|
|
.with_bypass_rules(bypass_rules)
|
|
.with_blocklist_file(blocklist_file)
|
|
.with_dns_allowlist_mode(dns_allowlist_mode)
|
|
.with_local_protocol(local_protocol)
|
|
.with_record_domains(record_domains);
|
|
save_proxy_config(&config)?;
|
|
|
|
// Log profile_id for debugging
|
|
if let Some(ref pid) = profile_id {
|
|
log::info!("Saved proxy config {} with profile_id: {}", id, pid);
|
|
} else {
|
|
log::info!("Saved proxy config {} without profile_id", id);
|
|
}
|
|
|
|
// Spawn proxy worker process in the background using std::process::Command
|
|
// This ensures proper process detachment on Unix systems
|
|
let exe = find_sidecar_executable("donut-proxy")?;
|
|
let temp_dir = std::env::temp_dir();
|
|
let log_path = temp_dir.join(format!("donut-proxy-{id}.log"));
|
|
let log_file = crate::app_dirs::create_owner_only(&log_path);
|
|
prune_stale_proxy_logs(&temp_dir, RETAINED_PROXY_LOGS);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::process::CommandExt;
|
|
use std::process::Command as StdCommand;
|
|
|
|
let mut cmd = StdCommand::new(&exe);
|
|
cmd.arg("proxy-worker");
|
|
cmd.arg("start");
|
|
cmd.arg("--id");
|
|
cmd.arg(&id);
|
|
cmd.env_remove("DONUT_PROXY_USERNAME");
|
|
cmd.env_remove("DONUT_PROXY_PASSWORD");
|
|
|
|
cmd.stdin(Stdio::null());
|
|
cmd.stdout(Stdio::null());
|
|
|
|
// Always log to file for diagnostics (both debug and release builds)
|
|
if let Ok(file) = log_file {
|
|
log::info!("Proxy worker stderr will be logged to: {:?}", log_path);
|
|
cmd.stderr(Stdio::from(file));
|
|
} else {
|
|
cmd.stderr(Stdio::null());
|
|
}
|
|
|
|
// Properly detach the process on Unix by creating a new session
|
|
unsafe {
|
|
cmd.pre_exec(|| {
|
|
// Create a new process group so the process survives parent exit
|
|
libc::setsid();
|
|
|
|
// Set high priority so the proxy is killed last under resource pressure
|
|
// Negative nice value = higher priority. Try -10, fall back to -5 if it fails.
|
|
if libc::setpriority(libc::PRIO_PROCESS, 0, -10) != 0 {
|
|
let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -5);
|
|
}
|
|
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
// Spawn detached process
|
|
let child = cmd.spawn()?;
|
|
let pid = child.id();
|
|
|
|
// Store PID
|
|
{
|
|
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
|
processes.insert(id.clone(), pid);
|
|
}
|
|
|
|
// Don't wait for the child - it's detached
|
|
drop(child);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::io::AsRawHandle;
|
|
use std::os::windows::process::CommandExt;
|
|
use std::process::Command as StdCommand;
|
|
use windows::Win32::Foundation::{CloseHandle, SetHandleInformation, HANDLE, HANDLE_FLAGS};
|
|
use windows::Win32::System::Threading::{
|
|
OpenProcess, SetPriorityClass, ABOVE_NORMAL_PRIORITY_CLASS, PROCESS_SET_INFORMATION,
|
|
};
|
|
|
|
// Mark current stdout/stderr as non-inheritable so the spawned worker process
|
|
// does not inherit pipe handles from our parent (prevents blocking when parent exits).
|
|
let stdout_handle = std::io::stdout().as_raw_handle();
|
|
let stderr_handle = std::io::stderr().as_raw_handle();
|
|
const HANDLE_FLAG_INHERIT: u32 = 0x00000001;
|
|
unsafe {
|
|
if !stdout_handle.is_null() {
|
|
let _ = SetHandleInformation(HANDLE(stdout_handle), HANDLE_FLAG_INHERIT, HANDLE_FLAGS(0));
|
|
}
|
|
if !stderr_handle.is_null() {
|
|
let _ = SetHandleInformation(HANDLE(stderr_handle), HANDLE_FLAG_INHERIT, HANDLE_FLAGS(0));
|
|
}
|
|
}
|
|
|
|
let mut cmd = StdCommand::new(&exe);
|
|
cmd.arg("proxy-worker");
|
|
cmd.arg("start");
|
|
cmd.arg("--id");
|
|
cmd.arg(&id);
|
|
cmd.env_remove("DONUT_PROXY_USERNAME");
|
|
cmd.env_remove("DONUT_PROXY_PASSWORD");
|
|
|
|
cmd.stdin(Stdio::null());
|
|
cmd.stdout(Stdio::null());
|
|
|
|
// Log to file for diagnostics (matching Unix behavior)
|
|
if let Ok(file) = log_file {
|
|
log::info!("Proxy worker stderr will be logged to: {:?}", log_path);
|
|
cmd.stderr(Stdio::from(file));
|
|
} else {
|
|
cmd.stderr(Stdio::null());
|
|
}
|
|
|
|
// On Windows, use DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP for proper detachment.
|
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
|
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
|
|
|
|
let child = cmd.spawn()?;
|
|
let pid = child.id();
|
|
|
|
// Set high priority so the proxy is killed last under resource pressure
|
|
unsafe {
|
|
if let Ok(handle) = OpenProcess(PROCESS_SET_INFORMATION, false, pid) {
|
|
let _ = SetPriorityClass(handle, ABOVE_NORMAL_PRIORITY_CLASS);
|
|
let _ = CloseHandle(handle);
|
|
}
|
|
}
|
|
|
|
// Store PID
|
|
{
|
|
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
|
processes.insert(id.clone(), pid);
|
|
}
|
|
|
|
drop(child);
|
|
}
|
|
|
|
// Give the process a moment to start up before checking
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
// Wait for the worker to bind to the port and update config
|
|
// Since we pre-allocated the port, the worker should bind immediately
|
|
// We check quickly with short intervals to make startup fast
|
|
let mut attempts = 0;
|
|
let max_attempts = 40; // 4 seconds max (40 * 100ms) - give it more time to start
|
|
|
|
loop {
|
|
// Use shorter sleep for faster startup
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
if let Some(updated_config) = get_proxy_config(&id) {
|
|
// Check if local_url is set (worker has bound and updated config)
|
|
if let Some(ref local_url) = updated_config.local_url {
|
|
if !local_url.is_empty() {
|
|
if let Some(port) = updated_config.local_port {
|
|
// Try to connect immediately - port should be ready since we pre-allocated it
|
|
match tokio::time::timeout(
|
|
tokio::time::Duration::from_millis(100),
|
|
tokio::net::TcpStream::connect(("127.0.0.1", port)),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Ok(_stream)) => {
|
|
// Port is listening and accepting connections!
|
|
return Ok(updated_config);
|
|
}
|
|
Ok(Err(_)) | Err(_) => {
|
|
// Port not ready yet, continue waiting
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
attempts += 1;
|
|
if attempts >= max_attempts {
|
|
// Try to get the config one more time for better error message
|
|
let detail = if let Some(config) = get_proxy_config(&id) {
|
|
// Check if process is still running
|
|
let process_running = config.pid.map(is_process_running).unwrap_or(false);
|
|
format!(
|
|
"Config: id={}, local_url={:?}, local_port={:?}, pid={:?}, process_running={}",
|
|
config.id, config.local_url, config.local_port, config.pid, process_running
|
|
)
|
|
} else {
|
|
format!("Config not found for id: {}", id)
|
|
};
|
|
// The detached worker (if it did spawn) would otherwise outlive this
|
|
// failed start with nothing tracking it — callers only get an error
|
|
// string, so this is the last place that can still kill it.
|
|
let _ = stop_proxy_process(&id).await;
|
|
return Err(format!("Proxy worker failed to start in time. {detail}").into());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn stop_proxy_process(id: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let pid = PROXY_PROCESSES
|
|
.lock()
|
|
.unwrap()
|
|
.get(id)
|
|
.copied()
|
|
.or_else(|| get_proxy_config(id).and_then(|config| config.pid));
|
|
|
|
if let Some(pid) = pid {
|
|
// Kill the process
|
|
#[cfg(unix)]
|
|
{
|
|
use std::process::Command;
|
|
let _ = Command::new("kill")
|
|
.arg("-TERM")
|
|
.arg(pid.to_string())
|
|
.output();
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
use std::process::Command;
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
let _ = Command::new("taskkill")
|
|
.args(["/F", "/PID", &pid.to_string()])
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
.output();
|
|
}
|
|
|
|
// Wait a bit for the process to exit
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
// Remove from tracking
|
|
{
|
|
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
|
processes.remove(id);
|
|
}
|
|
|
|
// Delete the config file
|
|
delete_proxy_config(id);
|
|
return Ok(true);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
pub async fn stop_all_proxy_processes() -> Result<(), Box<dyn std::error::Error>> {
|
|
let configs = list_proxy_configs();
|
|
for config in configs {
|
|
let _ = stop_proxy_process(&config.id).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
is_executable_file, parse_sidecar_version, prune_stale_proxy_logs, target_binary_name,
|
|
};
|
|
use std::fs;
|
|
use std::time::Duration;
|
|
|
|
#[test]
|
|
fn parses_exact_sidecar_version_output() {
|
|
assert_eq!(
|
|
parse_sidecar_version(b"donut-proxy v0.28.2\n").as_deref(),
|
|
Some("v0.28.2")
|
|
);
|
|
assert_eq!(
|
|
parse_sidecar_version(b"donut-proxy nightly-2026-07-19-a4ed5c8\r\n").as_deref(),
|
|
Some("nightly-2026-07-19-a4ed5c8")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_or_ambiguous_sidecar_version_output() {
|
|
assert_eq!(parse_sidecar_version(b""), None);
|
|
assert_eq!(parse_sidecar_version(b"donut-proxy"), None);
|
|
assert_eq!(parse_sidecar_version(b"other-proxy v0.28.2"), None);
|
|
assert_eq!(
|
|
parse_sidecar_version(b"donut-proxy v0.28.2\nunexpected"),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sidecar_target_name_is_exact_and_metadata_files_are_not_executables() {
|
|
let target_name = target_binary_name("xray");
|
|
assert!(target_name.starts_with("xray-"));
|
|
assert!(target_name.contains(env!("DONUT_BUILD_TARGET")));
|
|
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let marker = temp.path().join(format!("{target_name}.source.json"));
|
|
fs::write(&marker, "{}").unwrap();
|
|
assert!(!is_executable_file(&marker));
|
|
|
|
let executable = temp.path().join(target_name);
|
|
fs::write(&executable, "binary").unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
assert!(is_executable_file(&executable));
|
|
}
|
|
|
|
#[test]
|
|
fn prunes_only_old_proxy_logs() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
for id in ["oldest", "middle", "newest"] {
|
|
fs::write(temp.path().join(format!("donut-proxy-{id}.log")), id).unwrap();
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
fs::write(temp.path().join("unrelated.log"), "keep").unwrap();
|
|
|
|
prune_stale_proxy_logs(temp.path(), 2);
|
|
|
|
assert!(!temp.path().join("donut-proxy-oldest.log").exists());
|
|
assert!(temp.path().join("donut-proxy-middle.log").exists());
|
|
assert!(temp.path().join("donut-proxy-newest.log").exists());
|
|
assert!(temp.path().join("unrelated.log").exists());
|
|
}
|
|
}
|