use std::env;
use std::fs::{File, remove_file};
use std::io;
use std::process::{Command, exit};
use std::path::{Path, PathBuf};
use serde_json::Value;
use anyhow::{Result, Context}; use uuid::Uuid; use log::{info, debug, error};
pub const RSPAWN_VERSION: &str = env!("CARGO_PKG_VERSION");
fn generate_lock_file_path() -> PathBuf {
let lock_dir = "/tmp"; let lock_file_name = format!("{}.lock", Uuid::new_v4());
Path::new(lock_dir).join(lock_file_name)
}
struct LockFileGuard {
lock_file_path: PathBuf,
}
impl Drop for LockFileGuard {
fn drop(&mut self) {
if let Err(e) = remove_file(&self.lock_file_path) {
let error_msg = format!("Failed to remove lock file: {}", e);
eprintln!("{error_msg}");
error!("{error_msg}");
}
}
}
fn create_lock_file(lock_file_path: &Path) -> io::Result<()> {
File::create(lock_file_path).map(|_| ())
}
fn get_latest_version_from_crates_io(crate_name: &str) -> Result<String> {
let url = format!("https://crates.io/api/v1/crates/{}/versions", crate_name);
let user_agent = format!("rspawn/{RSPAWN_VERSION} (https://github.com/jgabaut/rspawn");
info!("Fetching latest version for {} from: {}", crate_name, url);
let client = reqwest::blocking::Client::new();
let response = client
.get(&url)
.header("User-Agent", user_agent)
.send()
.context("Failed to fetch from crates.io")?;
let status = response.status();
debug!("Response status: {}", status);
if !status.is_success() {
let error_msg = format!("Failed to fetch crate info: HTTP {}", status);
error!("{error_msg}");
return Err(anyhow::anyhow!("{error_msg}"));
}
let body = response.text().context("Failed to read response body")?;
debug!("Response body: {}", body);
let json: Value = serde_json::from_str(&body).context("Failed to parse JSON response")?;
debug!("Parsed JSON: {:?}", json);
let latest_version = json["versions"]
.as_array()
.and_then(|versions| versions.first())
.and_then(|version| version["num"].as_str())
.ok_or_else(|| anyhow::anyhow!("Failed to get the latest version"))?;
Ok(latest_version.to_string())
}
pub fn is_executed_from_path() -> bool {
let exe_path = env::current_exe().unwrap_or_else(|_| PathBuf::new());
if exe_path.is_relative() || exe_path.parent().is_none() {
return false;
}
if let Some(exe_name) = exe_path.file_name() {
let exe_name = exe_name.to_string_lossy();
for dir in env::var("PATH").unwrap_or_else(|_| String::new()).split(':') {
let full_path = Path::new(&dir).join(&*exe_name);
if full_path.exists() {
return true; }
}
}
false }
#[allow(non_snake_case)]
pub struct RSpawn<F>
where
F: FnMut(&str) -> bool + 'static,
{
active_features: Option<Vec<String>>,
user_confirm: Option<F>,
check_if_executed_from_PATH: Option<bool>,
}
impl<F> RSpawn<F>
where
F: FnMut(&str) -> bool + 'static,
{
pub fn new() -> Self {
RSpawn {
active_features: None,
user_confirm: None,
#[allow(non_snake_case)]
check_if_executed_from_PATH: Some(true),
}
}
pub fn active_features(mut self, active_features: Vec<String>) -> Self {
self.active_features = Some(active_features);
self
}
pub fn user_confirm(mut self, user_confirm: F) -> Self {
self.user_confirm = Some(user_confirm);
self
}
#[allow(non_snake_case)]
pub fn check_if_executed_from_PATH(mut self, check: bool) -> Self {
self.check_if_executed_from_PATH = Some(check);
self
}
pub fn relaunch_program(self) -> Result<()> {
let active_features = self.active_features.unwrap_or_default();
#[allow(non_snake_case)]
let check_if_executed_from_PATH = self.check_if_executed_from_PATH.unwrap_or(true);
let confirm_fn: Box<dyn FnMut(&str) -> bool> = if let Some(mut custom_confirm) = self.user_confirm {
Box::new(move |version| custom_confirm(version))
} else {
Box::new(default_user_confirm)
};
relaunch_program(Some(active_features), Some(confirm_fn), check_if_executed_from_PATH)
}
}
pub fn relaunch_program<F>(
active_features: Option<Vec<String>>,
user_confirm: Option<F>,
#[allow(non_snake_case)]
check_if_executed_from_PATH: bool
) -> Result<()>
where
F: FnMut(&str) -> bool + 'static,
{
let lock_file_path = generate_lock_file_path();
if lock_file_path.exists() {
return Err(anyhow::anyhow!("Program is already relaunching; avoiding infinite loop.").into());
}
create_lock_file(&lock_file_path).context("Failed to create lock file")?;
let _lock_guard = LockFileGuard {
lock_file_path,
};
if check_if_executed_from_PATH && !is_executed_from_path() {
return Err(anyhow::anyhow!("Program must be executed from PATH, not from a full or relative path.").into());
}
let crate_name = env!("CARGO_PKG_NAME").to_string();
let latest_version = get_latest_version_from_crates_io(&crate_name).context("Failed to get latest version")?;
let current_version = env!("CARGO_PKG_VERSION");
if latest_version != current_version {
let mut confirm_fn: Box<dyn FnMut(&str) -> bool> = if let Some(mut custom_confirm) = user_confirm {
Box::new(move |version| custom_confirm(version))
} else {
Box::new(default_user_confirm)
};
if confirm_fn(&latest_version) {
let mut install_command = {
let mut cmd = Command::new("cargo");
cmd.arg("install").arg(crate_name);
if let Some(features) = active_features {
if !features.is_empty() {
cmd.args(features.iter().flat_map(|f| ["--features", f]));
}
}
cmd };
let mut child = install_command.spawn()
.context("Failed to run cargo install")?;
let _ = child.wait().context("Failed to wait for cargo install")?;
let args: Vec<String> = env::args().collect();
let child = Command::new(&args[0])
.args(&args[1..]) .spawn();
match child {
Ok(_) => {
exit(0); },
Err(e) => {
return Err(anyhow::anyhow!("Failed to relaunch the program: {}", e).into());
}
}
} else {
info!("You chose not to update.");
}
} else {
info!("You are already using the latest version.");
}
Ok(())
}
fn default_user_confirm(version: &str) -> bool {
println!("A new version {} is available. Would you like to install it? (y/n): ", version);
let mut response = String::new();
io::stdin().read_line(&mut response).unwrap();
response.trim().to_lowercase() == "y"
}