use anyhow::{Context, Result};
use clap::Args;
use semver::Version;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(target_os = "macos")]
use super::service::generate_wrapper_script;
#[cfg(target_os = "linux")]
use super::service::{generate_system_service_file, generate_user_service_file};
const GITHUB_API_URL: &str = "https://api.github.com/repos/freenet/freenet-core/releases/latest";
pub const EXIT_CODE_ALREADY_UP_TO_DATE: i32 = 2;
#[derive(Args, Debug, Clone)]
pub struct UpdateCommand {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub quiet: bool,
}
impl UpdateCommand {
pub fn run(&self, current_version: &str) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(self.run_async(current_version))
}
async fn run_async(&self, current_version: &str) -> Result<()> {
if !self.quiet {
println!("Current version: {}", current_version);
println!("Checking for updates...");
}
let latest = get_latest_release().await?;
let latest_version = latest.tag_name.trim_start_matches('v');
if !self.quiet {
println!("Latest version: {}", latest_version);
}
let current_ver =
Version::parse(current_version).context("Failed to parse current version as semver")?;
let latest_ver =
Version::parse(latest_version).context("Failed to parse latest version as semver")?;
if !self.force && latest_ver <= current_ver {
if !self.quiet {
println!("You are already running the latest version.");
}
std::process::exit(EXIT_CODE_ALREADY_UP_TO_DATE);
}
if self.check {
if latest_ver > current_ver && !self.quiet {
println!(
"Update available: {} -> {}",
current_version, latest_version
);
}
return Ok(());
}
if !self.quiet {
println!("Downloading update...");
}
self.download_and_install(&latest).await
}
async fn download_and_install(&self, release: &Release) -> Result<()> {
let target = get_target_triple();
let extension = get_archive_extension();
let freenet_asset_name = format!("freenet-{}.{}", target, extension);
let fdev_asset_name = format!("fdev-{}.{}", target, extension);
let freenet_asset = release
.assets
.iter()
.find(|a| a.name == freenet_asset_name)
.ok_or_else(|| {
anyhow::anyhow!(
"No binary available for your platform ({}). Available assets: {}",
target,
release
.assets
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
let fdev_asset = release.assets.iter().find(|a| a.name == fdev_asset_name);
let checksums = if let Some(checksums_asset) =
release.assets.iter().find(|a| a.name == "SHA256SUMS.txt")
{
if !self.quiet {
println!("Downloading checksums...");
}
match download_checksums(&checksums_asset.browser_download_url).await {
Ok(c) => Some(c),
Err(e) => {
if !self.quiet {
eprintln!(
"Warning: Failed to download checksums: {}. Continuing without verification.",
e
);
}
None
}
}
} else {
if !self.quiet {
eprintln!(
"Warning: SHA256SUMS.txt not found in release. Continuing without checksum verification."
);
}
None
};
let temp_dir = tempfile::tempdir().context("Failed to create temp directory")?;
let freenet_archive_path = temp_dir.path().join(&freenet_asset_name);
download_file(
&freenet_asset.browser_download_url,
&freenet_archive_path,
self.quiet,
)
.await?;
if let Some(checksums) = &checksums {
if let Some(expected_hash) = checksums.get(&freenet_asset_name) {
if !self.quiet {
println!("Verifying freenet checksum...");
}
verify_checksum(&freenet_archive_path, expected_hash)?;
} else if !self.quiet {
eprintln!(
"Warning: Checksum not found for {}. Continuing without verification.",
freenet_asset_name
);
}
}
let freenet_extract_dir = temp_dir.path().join("freenet");
fs::create_dir_all(&freenet_extract_dir)?;
let extracted_freenet =
extract_binary(&freenet_archive_path, &freenet_extract_dir, "freenet")?;
let current_exe = std::env::current_exe().context("Failed to get current executable")?;
replace_binary(&extracted_freenet, ¤t_exe)?;
if !self.quiet {
println!(
"Successfully updated freenet to version {}",
release.tag_name.trim_start_matches('v')
);
}
if let Some(fdev_asset) = fdev_asset {
if !self.quiet {
println!("Downloading fdev...");
}
self.try_update_fdev(
fdev_asset,
&fdev_asset_name,
&checksums,
temp_dir.path(),
¤t_exe,
)
.await;
} else if !self.quiet {
eprintln!("Warning: fdev not found in release assets. Skipping fdev update.");
}
if let Err(e) = ensure_service_file_updated(¤t_exe, self.quiet) {
if !self.quiet {
eprintln!(
"Warning: Failed to update service file: {}. \
Run 'freenet service install' to update manually.",
e
);
}
}
if !self.quiet {
#[cfg(target_os = "linux")]
{
if is_systemd_service_active() {
println!("Restarting Freenet service...");
let status = Command::new("systemctl")
.args(["--user", "restart", "freenet"])
.status();
match status {
Ok(s) if s.success() => println!("Service restarted successfully."),
Ok(_) => eprintln!(
"Warning: Failed to restart service. Run 'freenet service restart' manually."
),
Err(e) => eprintln!(
"Warning: Failed to restart service: {}. Run 'freenet service restart' manually.",
e
),
}
}
}
#[cfg(target_os = "macos")]
{
if is_launchd_service_active() {
println!("Restarting Freenet service...");
if let Err(e) = Command::new("launchctl")
.args(["stop", "org.freenet.node"])
.status()
{
eprintln!("Warning: failed to stop service: {e}");
}
let status = Command::new("launchctl")
.args(["start", "org.freenet.node"])
.status();
match status {
Ok(s) if s.success() => println!("Service restarted successfully."),
Ok(_) => eprintln!(
"Warning: Failed to restart service. Run 'freenet service restart' manually."
),
Err(e) => eprintln!(
"Warning: Failed to restart service: {}. Run 'freenet service restart' manually.",
e
),
}
}
}
#[cfg(target_os = "windows")]
{
if is_windows_wrapper_running() {
println!("Restarting Freenet service...");
let our_pid = std::process::id().to_string();
Command::new("taskkill")
.args([
"/f",
"/im",
"freenet.exe",
"/fi",
&format!("PID ne {}", our_pid),
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.ok();
std::thread::sleep(std::time::Duration::from_secs(2));
let status = Command::new(¤t_exe)
.args(["service", "start"])
.status();
match status {
Ok(s) if s.success() => println!("Service restarted successfully."),
Ok(_) => eprintln!(
"Warning: Failed to restart service. Run 'freenet service start' manually."
),
Err(e) => eprintln!(
"Warning: Failed to restart service: {}. Run 'freenet service start' manually.",
e
),
}
}
}
}
Ok(())
}
async fn try_update_fdev(
&self,
asset: &Asset,
asset_name: &str,
checksums: &Option<Checksums>,
temp_dir: &Path,
freenet_exe: &Path,
) {
let archive_path = temp_dir.join(asset_name);
if let Err(e) = download_file(&asset.browser_download_url, &archive_path, self.quiet).await
{
if !self.quiet {
eprintln!(
"Warning: Failed to download fdev: {}. Skipping fdev update.",
e
);
}
return;
}
if let Some(checksums) = &checksums {
if let Some(expected_hash) = checksums.get(asset_name) {
if !self.quiet {
println!("Verifying fdev checksum...");
}
if let Err(e) = verify_checksum(&archive_path, expected_hash) {
if !self.quiet {
eprintln!(
"Warning: fdev checksum verification failed: {}. Skipping fdev update.",
e
);
}
return;
}
}
}
let extract_dir = temp_dir.join("fdev");
if let Err(e) = fs::create_dir_all(&extract_dir) {
if !self.quiet {
eprintln!(
"Warning: Failed to create fdev extract directory: {}. Skipping fdev update.",
e
);
}
return;
}
let extracted_fdev = match extract_binary(&archive_path, &extract_dir, "fdev") {
Ok(path) => path,
Err(e) => {
if !self.quiet {
eprintln!(
"Warning: Failed to extract fdev: {}. Skipping fdev update.",
e
);
}
return;
}
};
let Some(install_dir) = freenet_exe.parent() else {
if !self.quiet {
eprintln!("Warning: Cannot determine install directory. Skipping fdev update.");
}
return;
};
#[cfg(target_os = "windows")]
let fdev_dest = install_dir.join("fdev.exe");
#[cfg(not(target_os = "windows"))]
let fdev_dest = install_dir.join("fdev");
if let Err(e) = replace_binary(&extracted_fdev, &fdev_dest) {
if !self.quiet {
eprintln!(
"Warning: Failed to update fdev: {}. You can update it manually with: curl -fsSL https://freenet.org/install.sh | sh",
e
);
}
} else if !self.quiet {
println!("Successfully updated fdev.");
}
}
}
#[derive(serde::Deserialize, Debug)]
struct Release {
tag_name: String,
assets: Vec<Asset>,
}
#[derive(serde::Deserialize, Debug)]
struct Asset {
name: String,
browser_download_url: String,
}
struct Checksums {
entries: std::collections::HashMap<String, String>,
}
impl Checksums {
fn parse(content: &str) -> Self {
let mut entries = std::collections::HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let hash = parts[0].to_string();
let filename = parts[1].to_string();
entries.insert(filename, hash);
}
}
Self { entries }
}
fn get(&self, filename: &str) -> Option<&str> {
self.entries.get(filename).map(|s| s.as_str())
}
}
async fn get_latest_release() -> Result<Release> {
let client = reqwest::Client::builder()
.user_agent("freenet-updater")
.build()?;
let response = client
.get(GITHUB_API_URL)
.send()
.await
.context("Failed to fetch release info")?;
if !response.status().is_success() {
anyhow::bail!(
"GitHub API returned error: {} {}",
response.status(),
response.text().await.unwrap_or_default()
);
}
response
.json::<Release>()
.await
.context("Failed to parse release info")
}
async fn download_checksums(url: &str) -> Result<Checksums> {
let client = reqwest::Client::builder()
.user_agent("freenet-updater")
.build()?;
let response = client
.get(url)
.send()
.await
.context("Failed to download checksums")?;
if !response.status().is_success() {
anyhow::bail!("Failed to download checksums: {}", response.status());
}
let content = response.text().await.context("Failed to read checksums")?;
Ok(Checksums::parse(&content))
}
fn verify_checksum(file_path: &Path, expected_hash: &str) -> Result<()> {
use sha2::{Digest, Sha256};
use std::io::Read;
let mut file = File::open(file_path).context("Failed to open file for checksum")?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
loop {
let n = file
.read(&mut buf)
.context("Failed to read file for checksum")?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let result = hasher.finalize();
let actual_hash = result.iter().fold(String::with_capacity(64), |mut s, b| {
use std::fmt::Write;
write!(s, "{:02x}", b).expect("writing to String is infallible");
s
});
if actual_hash != expected_hash {
anyhow::bail!(
"Checksum verification failed!\nExpected: {}\nGot: {}\n\
The download may be corrupted or tampered with.",
expected_hash,
actual_hash
);
}
Ok(())
}
fn get_target_triple() -> &'static str {
#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
{
"x86_64-unknown-linux-musl"
}
#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
{
"aarch64-unknown-linux-musl"
}
#[cfg(all(target_arch = "x86_64", target_os = "macos"))]
{
"x86_64-apple-darwin"
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
{
"aarch64-apple-darwin"
}
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
{
"x86_64-pc-windows-msvc"
}
#[cfg(not(any(
all(target_arch = "x86_64", target_os = "linux"),
all(target_arch = "aarch64", target_os = "linux"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "macos"),
all(target_arch = "x86_64", target_os = "windows"),
)))]
{
"unknown"
}
}
fn get_archive_extension() -> &'static str {
#[cfg(target_os = "windows")]
{
"zip"
}
#[cfg(not(target_os = "windows"))]
{
"tar.gz"
}
}
async fn download_file(url: &str, dest: &Path, quiet: bool) -> Result<()> {
let client = reqwest::Client::builder()
.user_agent("freenet-updater")
.build()?;
let response = client
.get(url)
.send()
.await
.context("Failed to download file")?;
if !response.status().is_success() {
anyhow::bail!("Download failed: {}", response.status());
}
let total_size = response.content_length().unwrap_or(0);
let mut downloaded: u64 = 0;
let mut file = File::create(dest).context("Failed to create temp file")?;
let mut stream = response.bytes_stream();
use futures::StreamExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk.context("Error while downloading")?;
file.write_all(&chunk)?;
downloaded += chunk.len() as u64;
if !quiet && total_size > 0 {
let progress = (downloaded as f64 / total_size as f64 * 100.0) as u32;
print!("\rDownloading... {}%\x1b[K", progress);
io::stdout().flush()?;
}
}
if !quiet {
println!("\rDownload complete.\x1b[K");
}
Ok(())
}
fn extract_binary(archive_path: &Path, dest_dir: &Path, name: &str) -> Result<PathBuf> {
let dest_dir_canonical = dest_dir
.canonicalize()
.context("Failed to canonicalize dest dir")?;
let is_zip = archive_path
.extension()
.map(|ext| ext == "zip")
.unwrap_or(false);
if is_zip {
extract_zip(archive_path, dest_dir, &dest_dir_canonical)?;
} else {
extract_tar_gz(archive_path, dest_dir, &dest_dir_canonical)?;
}
#[cfg(target_os = "windows")]
let binary_name = format!("{name}.exe");
#[cfg(not(target_os = "windows"))]
let binary_name = name.to_string();
let binary_path = dest_dir.join(&binary_name);
if !binary_path.exists() {
anyhow::bail!("{name} binary not found in archive");
}
verify_binary(&binary_path)?;
Ok(binary_path)
}
fn extract_tar_gz(archive_path: &Path, dest_dir: &Path, dest_dir_canonical: &Path) -> Result<()> {
let file = File::open(archive_path).context("Failed to open archive")?;
let decoder = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(decoder);
for entry in archive
.entries()
.context("Failed to read archive entries")?
{
let mut entry = entry.context("Failed to read archive entry")?;
let path = entry.path().context("Failed to get entry path")?;
validate_extract_path(dest_dir, dest_dir_canonical, &path)?;
entry
.unpack_in(dest_dir)
.context("Failed to extract entry")?;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn extract_zip(archive_path: &Path, dest_dir: &Path, dest_dir_canonical: &Path) -> Result<()> {
use std::io::Read;
let file = File::open(archive_path).context("Failed to open archive")?;
let mut archive = zip::ZipArchive::new(file).context("Failed to read zip archive")?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).context("Failed to read zip entry")?;
let outpath = match file.enclosed_name() {
Some(path) => dest_dir.join(path),
None => continue,
};
let outpath_str = outpath.to_string_lossy();
validate_extract_path(dest_dir, dest_dir_canonical, Path::new(&*outpath_str))?;
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
fn extract_zip(_archive_path: &Path, _dest_dir: &Path, _dest_dir_canonical: &Path) -> Result<()> {
anyhow::bail!("Zip extraction is only supported on Windows")
}
fn validate_extract_path(dest_dir: &Path, dest_dir_canonical: &Path, path: &Path) -> Result<()> {
let entry_dest = dest_dir.join(path);
let entry_canonical = entry_dest
.canonicalize()
.unwrap_or_else(|_| entry_dest.clone());
let check_path = if entry_canonical.exists() {
entry_canonical
} else {
entry_dest
.parent()
.and_then(|p| p.canonicalize().ok())
.unwrap_or_else(|| dest_dir_canonical.to_path_buf())
};
if !check_path.starts_with(dest_dir_canonical) {
anyhow::bail!(
"Security error: archive contains path traversal attempt: {}",
path.display()
);
}
Ok(())
}
fn verify_binary(binary_path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(binary_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(binary_path, perms)?;
}
let output = Command::new(binary_path)
.arg("--version")
.output()
.context("Failed to execute downloaded binary for verification")?;
if !output.status.success() {
anyhow::bail!(
"Downloaded binary failed verification (--version check failed). \
This could indicate a corrupted download or wrong architecture."
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.contains("Freenet") && !stdout.contains("freenet") {
anyhow::bail!(
"Downloaded binary doesn't appear to be Freenet. \
Got: {}",
stdout.trim()
);
}
Ok(())
}
#[cfg(target_os = "linux")]
fn ensure_service_file_updated(binary_path: &Path, quiet: bool) -> Result<()> {
let home_dir = dirs::home_dir();
let user_service_path = home_dir
.as_ref()
.map(|h| h.join(".config/systemd/user/freenet.service"));
if let Some(ref service_path) = user_service_path {
if service_path.exists() {
return update_service_file(binary_path, service_path, false, quiet);
}
}
let system_service_path = Path::new("/etc/systemd/system/freenet.service");
if system_service_path.exists() {
return update_service_file(binary_path, system_service_path, true, quiet);
}
Ok(())
}
#[cfg(target_os = "linux")]
fn update_service_file(
binary_path: &Path,
service_path: &Path,
system_mode: bool,
quiet: bool,
) -> Result<()> {
let content = fs::read_to_string(service_path).context("Failed to read service file")?;
if content.contains("ExecStopPost=")
&& content.contains("SuccessExitStatus=42")
&& content.contains("RestartPreventExitStatus=43")
{
return Ok(()); }
if !quiet {
println!("Updating service file to add auto-update support...");
}
let backup_path = service_path.with_extension("service.bak");
if let Err(e) = fs::copy(service_path, &backup_path) {
if !quiet {
eprintln!(
"Warning: Failed to backup service file: {}. Continuing anyway.",
e
);
}
} else if !quiet {
println!("Backed up existing service file to {:?}", backup_path);
}
let new_content = if system_mode {
let username = content
.lines()
.find_map(|l| l.strip_prefix("User="))
.unwrap_or("freenet");
let home_dir = content
.lines()
.find_map(|l| l.strip_prefix("Environment=HOME="))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(format!("/home/{username}")));
let log_dir = home_dir.join(".local/state/freenet");
generate_system_service_file(binary_path, &log_dir, username, &home_dir)
} else {
let home_dir = dirs::home_dir().context("Failed to get home directory")?;
let log_dir = home_dir.join(".local/state/freenet");
generate_user_service_file(binary_path, &log_dir)
};
fs::write(service_path, new_content).context("Failed to write updated service file")?;
let mut cmd = Command::new("systemctl");
if !system_mode {
cmd.arg("--user");
}
cmd.arg("daemon-reload");
let status = cmd.status().context("Failed to reload systemd")?;
if !status.success() && !quiet {
if system_mode {
eprintln!(
"Warning: Failed to reload systemd daemon. Run 'systemctl daemon-reload' manually."
);
} else {
eprintln!(
"Warning: Failed to reload systemd daemon. Run 'systemctl --user daemon-reload' manually."
);
}
} else if !quiet {
println!("Service file updated with auto-update hook.");
}
Ok(())
}
#[cfg(target_os = "macos")]
fn ensure_service_file_updated(binary_path: &Path, quiet: bool) -> Result<()> {
let home_dir = match dirs::home_dir() {
Some(dir) => dir,
None => return Ok(()), };
let plist_path = home_dir.join("Library/LaunchAgents/org.freenet.node.plist");
if !plist_path.exists() {
return Ok(());
}
let wrapper_path = home_dir.join(".local/bin/freenet-service-wrapper.sh");
let needs_update = if wrapper_path.exists() {
let content = fs::read_to_string(&wrapper_path).context("Failed to read wrapper script")?;
!content.contains("EXIT_CODE=$?") || !content.contains("freenet update")
} else {
true
};
if !needs_update {
return Ok(());
}
if !quiet {
println!("Updating service wrapper to add auto-update support...");
}
let wrapper_dir = wrapper_path
.parent()
.context("Wrapper path has no parent directory")?;
fs::create_dir_all(wrapper_dir).context("Failed to create wrapper directory")?;
if wrapper_path.exists() {
let backup_path = wrapper_path.with_extension("sh.bak");
if let Err(e) = fs::copy(&wrapper_path, &backup_path) {
if !quiet {
eprintln!(
"Warning: Failed to backup wrapper script: {}. Continuing anyway.",
e
);
}
} else if !quiet {
println!("Backed up existing wrapper script to {:?}", backup_path);
}
}
let wrapper_content = generate_wrapper_script(binary_path);
fs::write(&wrapper_path, &wrapper_content).context("Failed to write wrapper script")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&wrapper_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&wrapper_path, perms)?;
}
if !quiet {
println!("Service wrapper updated with auto-update hook.");
}
Ok(())
}
#[cfg(target_os = "windows")]
fn ensure_service_file_updated(binary_path: &Path, quiet: bool) -> Result<()> {
let exe_path_str = binary_path
.to_str()
.context("Executable path contains invalid UTF-8")?;
let run_command = format!("\"{}\" service run-wrapper", exe_path_str);
let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
let current_value = hkcu
.open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run")
.ok()
.and_then(|k| k.get_value::<String, _>("Freenet").ok());
let needs_update = match ¤t_value {
None => {
let has_legacy_task = std::process::Command::new("schtasks")
.args(["/query", "/tn", "Freenet"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
has_legacy_task
}
Some(val) => !val.contains("run-wrapper"),
};
if !needs_update {
return Ok(());
}
if !quiet {
println!("Migrating Freenet autostart to registry Run key...");
}
let (run_key, _) = hkcu
.create_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run")
.context("Failed to open registry Run key")?;
run_key
.set_value("Freenet", &run_command)
.context("Failed to write Freenet registry entry")?;
drop(
std::process::Command::new("schtasks")
.args(["/delete", "/tn", "Freenet", "/f"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status(),
);
if !quiet {
println!("Freenet autostart migrated successfully.");
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn ensure_service_file_updated(_binary_path: &Path, _quiet: bool) -> Result<()> {
Ok(())
}
fn replace_binary(new_binary: &Path, dest: &Path) -> Result<()> {
let backup_path = dest.with_extension("old");
let parent_dir = dest
.parent()
.context("Destination path has no parent directory")?;
if backup_path.exists() {
fs::remove_file(&backup_path).context("Failed to remove old backup")?;
}
let file_stem = dest.file_name().unwrap_or_default().to_string_lossy();
let temp_new = parent_dir.join(format!(".{file_stem}.new.tmp"));
fs::copy(new_binary, &temp_new).context("Failed to copy new binary to target directory")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&temp_new)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&temp_new, perms)?;
}
if dest.exists() {
fs::rename(dest, &backup_path)
.context("Failed to backup current binary. You may need to run with sudo.")?;
}
if let Err(e) = fs::rename(&temp_new, dest) {
if backup_path.exists() {
if let Err(restore_err) = fs::rename(&backup_path, dest) {
eprintln!(
"CRITICAL: Failed to restore backup after update failure. \
Original binary may be at: {}",
backup_path.display()
);
eprintln!("Restore error: {}", restore_err);
}
}
drop(fs::remove_file(&temp_new));
return Err(e).context("Failed to install new binary");
}
if backup_path.exists() {
if let Err(e) = fs::remove_file(&backup_path) {
eprintln!(
"Warning: Failed to remove backup file {}: {}",
backup_path.display(),
e
);
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn is_systemd_service_active() -> bool {
std::process::Command::new("systemctl")
.args(["--user", "is-active", "--quiet", "freenet"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn is_launchd_service_active() -> bool {
std::process::Command::new("launchctl")
.args(["list", "org.freenet.node"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
fn is_windows_wrapper_running() -> bool {
std::process::Command::new("tasklist")
.args(["/fi", "imagename eq freenet.exe", "/fo", "csv", "/nh"])
.output()
.map(|o| {
let stdout = String::from_utf8_lossy(&o.stdout);
stdout.matches("freenet.exe").count() > 1
})
.unwrap_or(false)
}