use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use super::detection::get_install_dir;
use crate::commands::service::DASHBOARD_URL;
#[derive(Debug, Clone)]
pub enum InstallProgress {
StoppingExisting,
CopyingBinary,
DownloadingFdev,
FdevSkipped(String),
AddingToPath,
InstallingService,
LaunchingService,
OpeningDashboard,
Complete,
Error(String),
}
pub fn run_install(progress: impl Fn(InstallProgress) + Send) -> Result<()> {
let install_dir =
get_install_dir().context("Could not determine install directory (%LOCALAPPDATA%)")?;
let current_exe =
std::env::current_exe().context("Could not determine current executable path")?;
let installed_exe = install_dir.join("freenet.exe");
progress(InstallProgress::StoppingExisting);
if installed_exe.exists() {
drop(
std::process::Command::new(&installed_exe)
.args(["service", "stop"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status(),
);
std::thread::sleep(std::time::Duration::from_secs(2));
}
progress(InstallProgress::CopyingBinary);
std::fs::create_dir_all(&install_dir)
.with_context(|| format!("Failed to create directory: {}", install_dir.display()))?;
std::fs::copy(¤t_exe, &installed_exe)
.with_context(|| format!("Failed to copy binary to {}", installed_exe.display()))?;
progress(InstallProgress::DownloadingFdev);
match download_fdev(&install_dir) {
Ok(()) => {}
Err(e) => {
progress(InstallProgress::FdevSkipped(format!("{e:#}")));
}
}
progress(InstallProgress::AddingToPath);
if let Err(e) = add_to_user_path(&install_dir) {
eprintln!("Warning: could not add to PATH: {e:#}");
}
progress(InstallProgress::InstallingService);
let output = std::process::Command::new(&installed_exe)
.args(["service", "install"])
.output()
.context("Failed to run freenet service install")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if stderr.trim().is_empty() {
format!("exit code {}", output.status.code().unwrap_or(-1))
} else {
stderr.trim().to_string()
};
return Err(anyhow::anyhow!("Service installation failed: {detail}"));
}
progress(InstallProgress::LaunchingService);
let status = std::process::Command::new(&installed_exe)
.args(["service", "start"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("Failed to start freenet service")?;
if !status.success() {
return Err(anyhow::anyhow!(
"Service failed to start (exit code {}). Try running 'freenet service start' manually.",
status.code().unwrap_or(-1)
));
}
std::thread::sleep(std::time::Duration::from_secs(3));
progress(InstallProgress::OpeningDashboard);
crate::commands::open_url_in_browser(DASHBOARD_URL);
progress(InstallProgress::Complete);
Ok(())
}
fn download_fdev(install_dir: &Path) -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let url = format!(
"https://github.com/freenet/freenet-core/releases/download/v{version}/fdev-x86_64-pc-windows-msvc.zip"
);
let zip_path = install_dir.join("fdev-download.zip");
download_url_to_file(&url, &zip_path)?;
let zip_file = std::fs::File::open(&zip_path).context("Failed to open downloaded fdev zip")?;
let mut archive = zip::ZipArchive::new(zip_file).context("Failed to read fdev zip")?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let name = file.name().to_string();
if name.ends_with("fdev.exe") {
let dest = install_dir.join("fdev.exe");
let mut out = std::fs::File::create(&dest).context("Failed to create fdev.exe")?;
std::io::copy(&mut file, &mut out)?;
break;
}
}
drop(std::fs::remove_file(&zip_path));
Ok(())
}
#[cfg(target_os = "windows")]
fn download_url_to_file(url: &str, dest: &Path) -> Result<()> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
#[link(name = "urlmon")]
unsafe extern "system" {
fn URLDownloadToFileW(
caller: *mut std::ffi::c_void,
url: *const u16,
file_name: *const u16,
reserved: u32,
status_cb: *mut std::ffi::c_void,
) -> i32;
}
let url_wide: Vec<u16> = OsStr::new(url).encode_wide().chain(Some(0)).collect();
let dest_wide: Vec<u16> = dest.as_os_str().encode_wide().chain(Some(0)).collect();
let hr = unsafe {
URLDownloadToFileW(
std::ptr::null_mut(),
url_wide.as_ptr(),
dest_wide.as_ptr(),
0,
std::ptr::null_mut(),
)
};
if hr != 0 {
anyhow::bail!("URLDownloadToFileW failed with HRESULT 0x{hr:08x}");
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
fn download_url_to_file(_url: &str, _dest: &Path) -> Result<()> {
anyhow::bail!("URLDownloadToFile is only available on Windows")
}
#[cfg(target_os = "windows")]
fn add_to_user_path(dir: &Path) -> Result<()> {
#[rustfmt::skip] use winreg::{RegKey, enums::*};
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
let raw_val = env
.get_raw_value("Path")
.unwrap_or_else(|_| winreg::RegValue {
bytes: std::borrow::Cow::Owned(Vec::new()),
vtype: REG_EXPAND_SZ,
});
let current_path = String::from_utf16_lossy(
&raw_val
.bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect::<Vec<u16>>(),
)
.trim_end_matches('\0')
.to_string();
let dir_str = dir.to_string_lossy();
let already_present = current_path
.split(';')
.any(|entry| entry.trim().eq_ignore_ascii_case(dir_str.as_ref()));
if already_present {
return Ok(());
}
let new_path = if current_path.is_empty() {
dir_str.to_string()
} else {
format!("{current_path};{dir_str}")
};
let mut new_bytes: Vec<u8> = new_path
.encode_utf16()
.chain(Some(0)) .flat_map(|c| c.to_le_bytes())
.collect();
new_bytes.extend_from_slice(&[0, 0]);
env.set_raw_value(
"Path",
&winreg::RegValue {
bytes: std::borrow::Cow::Owned(new_bytes),
vtype: raw_val.vtype, },
)?;
broadcast_environment_change();
Ok(())
}
#[cfg(not(target_os = "windows"))]
fn add_to_user_path(_dir: &Path) -> Result<()> {
Ok(()) }
#[cfg(target_os = "windows")]
fn broadcast_environment_change() {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let environment: Vec<u16> = OsStr::new("Environment")
.encode_wide()
.chain(Some(0))
.collect();
unsafe {
winapi::um::winuser::SendMessageTimeoutW(
winapi::um::winuser::HWND_BROADCAST,
winapi::um::winuser::WM_SETTINGCHANGE,
0,
environment.as_ptr() as isize,
winapi::um::winuser::SMTO_ABORTIFHUNG,
5000,
std::ptr::null_mut(),
);
}
}