use anyhow::{Context, Result};
use directories::ProjectDirs;
use freenet::tracing::tracer::get_log_dir;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use super::linux::home_dir_for_user;
use super::wrapper::log_wrapper_event;
pub(super) fn doctor_log_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
#[cfg(target_os = "macos")]
{
home.join("Library/Logs/freenet")
}
#[cfg(not(target_os = "macos"))]
{
home.join(".local/state/freenet")
}
}
#[cfg(unix)]
fn reap_stale_freenet_processes_escalating(log_dir: &Path) -> usize {
let uid = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.unwrap_or_default();
let escalated = kill_pattern_escalating(uid.trim(), "freenet network", 12);
if escalated > 0 {
log_wrapper_event(
log_dir,
"doctor: stale freenet network process(es) ignored SIGTERM, sent SIGKILL",
);
} else {
log_wrapper_event(log_dir, "doctor: reaped stale freenet network process(es)");
}
escalated
}
#[cfg(unix)]
pub(super) fn kill_pattern_escalating(uid: &str, pattern: &str, grace_secs: usize) -> usize {
use std::time::Duration;
std::process::Command::new("pkill")
.args(["-f", "-u", uid, pattern])
.status()
.ok();
for _ in 0..grace_secs {
std::thread::sleep(Duration::from_secs(1));
if pids_matching(uid, pattern).is_empty() {
return 0;
}
}
let survivors = pids_matching(uid, pattern).len();
if survivors > 0 {
std::process::Command::new("pkill")
.args(["-9", "-f", "-u", uid, pattern])
.status()
.ok();
std::thread::sleep(Duration::from_secs(1));
}
survivors
}
#[cfg(unix)]
pub(super) fn pids_matching(uid: &str, pattern: &str) -> Vec<u32> {
std::process::Command::new("pgrep")
.args(["-f", "-u", uid, pattern])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| {
s.split_whitespace()
.filter_map(|p| p.parse().ok())
.collect()
})
.unwrap_or_default()
}
pub(super) fn service_doctor(system: bool) -> Result<()> {
println!("freenet service doctor: recovering service install...");
println!(" - Re-templating service wrapper/unit to the current binary...");
super::install_service(system)?;
println!(" - Stopping the managed service...");
if let Err(e) = super::stop_service(system) {
println!(" (stop reported: {e} — continuing; the service may already be down)");
}
#[cfg(unix)]
{
let log_dir = doctor_log_dir();
println!(" - Reaping stale 'freenet network' processes...");
let escalated = reap_stale_freenet_processes_escalating(&log_dir);
if escalated > 0 {
println!(" ({escalated} process(es) ignored SIGTERM and were force-killed)");
}
}
#[cfg(target_os = "windows")]
{
println!(" - Reaping stale 'freenet network' processes...");
super::wrapper::kill_stale_freenet_processes(&doctor_log_dir());
}
println!(" - Starting the service...");
super::start_service(system)?;
println!("freenet service doctor: done. The service has been re-templated and restarted.");
println!(
"If the dashboard still shows an old version, hard-refresh the page to clear cached assets."
);
Ok(())
}
pub fn should_purge(purge: bool, keep_data: bool) -> Result<bool> {
if purge {
return Ok(true);
}
if keep_data {
return Ok(false);
}
use std::io::{self, BufRead, IsTerminal, Write};
if io::stdin().is_terminal() {
print!("Also remove all Freenet data, config, and logs? [y/N] ");
io::stdout().flush()?;
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
let answer = line.trim().to_ascii_lowercase();
Ok(answer == "y" || answer == "yes")
} else {
println!(
"Non-interactive mode: keeping data. Use --purge to also remove data, config, and logs."
);
Ok(false)
}
}
pub(super) fn remove_if_exists(label: &str, path: &Path) -> Result<()> {
if path.exists() {
println!("Removing {label}: {}", path.display());
std::fs::remove_dir_all(path)
.with_context(|| format!("Failed to remove {label} directory: {}", path.display()))?;
}
Ok(())
}
pub fn remove_dir_if_empty_pub(path: &Path) {
remove_dir_if_empty(path)
}
pub(super) fn remove_dir_if_empty(path: &Path) {
if !path.is_dir() {
return;
}
let Ok(mut entries) = std::fs::read_dir(path) else {
return;
};
if entries.next().is_some() {
return;
}
match std::fs::remove_dir(path) {
Ok(()) => println!("Removing empty dir: {}", path.display()),
Err(err) => {
eprintln!("Note: could not remove empty dir {}: {err}", path.display());
}
}
}
pub fn purge_data(system_mode: bool) -> Result<()> {
purge_data_dirs(system_mode)
}
pub(super) fn purge_data_dirs(#[allow(unused_variables)] system_mode: bool) -> Result<()> {
#[cfg(target_os = "linux")]
let home_override: Option<std::path::PathBuf> = if system_mode {
std::env::var("SUDO_USER")
.ok()
.map(|u| home_dir_for_user(&u))
} else {
None
};
#[cfg(not(target_os = "linux"))]
let home_override: Option<std::path::PathBuf> = None;
if let Some(ref home) = home_override {
for (label, dir) in linux_system_purge_dirs(home) {
remove_if_exists(label, &dir)?;
}
} else {
let leaves = DataLeaves::from_project_dirs();
purge_leaves_and_collapse(&leaves)?;
}
Ok(())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(super) fn linux_system_purge_dirs(home: &Path) -> [(&'static str, PathBuf); 4] {
[
("data", home.join(".local/share/freenet")),
("config", home.join(".config/freenet")),
("cache", home.join(".cache/freenet")),
("logs", home.join(".local/state/freenet")),
]
}
#[derive(Debug, Default, Clone)]
pub(super) struct DataLeaves {
pub(super) data_local: Option<PathBuf>,
pub(super) data_roaming: Option<PathBuf>,
pub(super) config: Option<PathBuf>,
pub(super) config_local: Option<PathBuf>,
pub(super) cache: Option<PathBuf>,
pub(super) cache_lowercase: Option<PathBuf>,
pub(super) log: Option<PathBuf>,
pub(super) collapse_parents: bool,
}
impl DataLeaves {
pub(super) fn from_project_dirs() -> Self {
let mut leaves = DataLeaves::default();
if let Some(dirs) = ProjectDirs::from("", "The Freenet Project Inc", "Freenet") {
let data_dir = dirs.data_local_dir().to_path_buf();
leaves.data_local = Some(data_dir.clone());
let roaming = dirs.data_dir().to_path_buf();
if roaming != data_dir {
leaves.data_roaming = Some(roaming.clone());
}
let config_dir = dirs.config_dir().to_path_buf();
if config_dir != data_dir && Some(&config_dir) != leaves.data_roaming.as_ref() {
leaves.config = Some(config_dir);
}
let config_local_dir = dirs.config_local_dir().to_path_buf();
if config_local_dir != data_dir
&& Some(&config_local_dir) != leaves.data_roaming.as_ref()
&& Some(&config_local_dir) != leaves.config.as_ref()
{
leaves.config_local = Some(config_local_dir);
}
leaves.cache = Some(dirs.cache_dir().to_path_buf());
} else {
eprintln!(
"Warning: Could not determine Freenet directories. Data and config may not have been removed."
);
}
if let Some(dirs) = ProjectDirs::from("", "The Freenet Project Inc", "freenet") {
let cache_lower = dirs.cache_dir().to_path_buf();
if cache_lower.exists() {
leaves.cache_lowercase = Some(cache_lower);
}
}
leaves.log = get_log_dir();
leaves.collapse_parents = cfg!(target_os = "windows");
leaves
}
}
pub(super) fn purge_leaves_and_collapse(leaves: &DataLeaves) -> Result<()> {
let mut parents: Vec<PathBuf> = Vec::new();
let collect = |leaf: &Path, acc: &mut Vec<PathBuf>| {
if leaves.collapse_parents {
push_parent(leaf, acc);
}
};
if let Some(ref data_local) = leaves.data_local {
remove_if_exists("data", data_local)?;
collect(data_local, &mut parents);
}
if let Some(ref roaming) = leaves.data_roaming {
remove_if_exists("data (legacy roaming)", roaming)?;
collect(roaming, &mut parents);
}
if let Some(ref config) = leaves.config {
remove_if_exists("config (legacy roaming)", config)?;
collect(config, &mut parents);
}
if let Some(ref config_local) = leaves.config_local {
remove_if_exists("config", config_local)?;
collect(config_local, &mut parents);
}
if let Some(ref cache) = leaves.cache {
remove_if_exists("cache", cache)?;
collect(cache, &mut parents);
}
if let Some(ref cache_lower) = leaves.cache_lowercase {
remove_if_exists("cache", cache_lower)?;
collect(cache_lower, &mut parents);
}
if let Some(ref log) = leaves.log {
remove_if_exists("logs", log)?;
collect(log, &mut parents);
}
parents.sort();
parents.dedup();
for parent in parents.into_iter().rev() {
remove_dir_if_empty(&parent);
}
Ok(())
}
fn push_parent(leaf: &Path, acc: &mut Vec<PathBuf>) {
if let Some(parent) = leaf.parent() {
acc.push(parent.to_path_buf());
}
}