use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInstanceInfo {
pub pid: u32,
pub port: u16,
pub owner: String,
pub instance_token: String,
pub started_at: String,
}
pub fn get_current_os_username() -> String {
std::env::var("USERNAME")
.or_else(|_| std::env::var("USER"))
.unwrap_or_else(|_| "user".to_string())
}
pub fn generate_instance_token() -> String {
use rand::RngCore;
let mut bytes = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut bytes);
hex::encode(bytes)
}
pub fn get_instance_file_path() -> Option<PathBuf> {
#[cfg(windows)]
{
dirs::config_dir().map(|d| d.join("Brum").join("run").join("instance.json"))
}
#[cfg(not(windows))]
{
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
let p = PathBuf::from(runtime_dir).join("brum").join("instance.json");
return Some(p);
}
dirs::data_local_dir()
.or_else(dirs::config_dir)
.map(|d| d.join("brum").join("run").join("instance.json"))
}
}
pub fn register_user_instance(port: u16, instance_token: &str) -> std::io::Result<Option<PathBuf>> {
if let Some(path) = get_instance_file_path() {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(parent, fs::Permissions::from_mode(0o700));
}
}
let info = UserInstanceInfo {
pid: std::process::id(),
port,
owner: get_current_os_username(),
instance_token: instance_token.to_string(),
started_at: chrono::Utc::now().to_rfc3339(),
};
let json = serde_json::to_string_pretty(&info)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
use std::io::Write;
let mut file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o600)
.open(&path)?;
file.write_all(json.as_bytes())?;
}
#[cfg(not(unix))]
{
fs::write(&path, json)?;
}
return Ok(Some(path));
}
Ok(None)
}
pub fn is_loopback_host(host: &str) -> bool {
let clean = host.trim().to_lowercase();
if clean.is_empty() || clean == "localhost" {
return true;
}
if let Ok(ip) = clean.parse::<std::net::IpAddr>() {
ip.is_loopback()
} else {
false
}
}
pub fn assert_loopback_bind(host: &str) -> Result<(), std::io::Error> {
if !is_loopback_host(host) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"Standalone desktop instance cannot bind to non-loopback address '{}'. \
Instance tokens grant administrative system privileges and must only be served on loopback (127.0.0.1, ::1, or localhost).",
host
),
));
}
Ok(())
}
pub fn unregister_user_instance() {
if let Some(path) = get_instance_file_path() {
if path.is_file() {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(info) = serde_json::from_str::<UserInstanceInfo>(&content) {
if info.pid == std::process::id() {
let _ = fs::remove_file(path);
}
}
}
}
}
}
pub fn get_active_user_instance() -> Option<UserInstanceInfo> {
let path = get_instance_file_path()?;
if !path.is_file() {
return None;
}
let data = fs::read_to_string(&path).ok()?;
let info: UserInstanceInfo = serde_json::from_str(&data).ok()?;
if !is_process_alive(info.pid) {
let _ = fs::remove_file(&path);
return None;
}
Some(info)
}
pub fn is_process_alive(pid: u32) -> bool {
#[cfg(windows)]
{
use windows_sys::Win32::Foundation::{CloseHandle, FALSE, STILL_ACTIVE};
use windows_sys::Win32::System::Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if handle == 0 as _ {
return false;
}
let mut exit_code: u32 = 0;
let ok = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
ok != 0 && exit_code == STILL_ACTIVE as u32
}
}
#[cfg(unix)]
{
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
}