use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::model::DEFAULT_PORT;
pub const PORT_ENV: &str = "SCSH_DAEMON_PORT";
pub const HOME_ENV: &str = "SCSH_HOME";
pub fn scsh_home_dir() -> PathBuf {
if let Some(dir) = std::env::var_os(HOME_ENV).filter(|s| !s.is_empty()) {
return PathBuf::from(dir);
}
match std::env::var_os("HOME").filter(|s| !s.is_empty()) {
Some(home) => PathBuf::from(home).join(".scsh"),
None => daemon_dir(),
}
}
pub fn store_db_file(port: u16) -> PathBuf {
scsh_home_dir().join(format!("daemon-{port}.redb"))
}
pub fn now_unix_secs() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
pub fn daemon_port() -> u16 {
std::env::var(PORT_ENV).ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_PORT)
}
pub fn daemon_dir() -> PathBuf {
std::env::temp_dir().join("scsh-daemon")
}
pub fn pid_file(port: u16) -> PathBuf {
daemon_dir().join(format!("daemon-{port}.pid"))
}
pub fn prune_file(port: u16) -> PathBuf {
daemon_dir().join(format!("prune-{port}.json"))
}
pub fn mode_file(port: u16) -> PathBuf {
daemon_dir().join(format!("daemon-{port}.mode"))
}
pub fn daemon_port_reachable(port: u16) -> bool {
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().expect("valid localhost address");
TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok()
}
pub fn daemon_api_responds(port: u16) -> bool {
if !daemon_port_reachable(port) {
return false;
}
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().expect("valid localhost address");
let Ok(mut stream) = TcpStream::connect_timeout(&addr, Duration::from_millis(500)) else {
return false;
};
stream.set_read_timeout(Some(Duration::from_secs(2))).ok();
stream.set_write_timeout(Some(Duration::from_secs(2))).ok();
let req = "GET /api/v1/sessions HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n";
if stream.write_all(req.as_bytes()).is_err() {
return false;
}
let mut resp = String::new();
if stream.read_to_string(&mut resp).is_err() {
return false;
}
resp.starts_with("HTTP/1.1 200") && resp.contains("application/json")
}
pub fn read_persisted_mode(port: u16) -> Option<super::model::DaemonMode> {
let text = std::fs::read_to_string(mode_file(port)).ok()?;
super::model::DaemonMode::parse(text.trim())
}
pub fn write_mode_marker(port: u16, mode: super::model::DaemonMode) {
let _ = std::fs::create_dir_all(daemon_dir());
let _ = std::fs::write(mode_file(port), mode.as_str());
}
pub fn signal_process(pid: u32, sig: i32) {
#[cfg(unix)]
{
unsafe {
libc::kill(pid as i32, sig);
}
}
}
pub fn pid_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
#[cfg(unix)]
{
unsafe { libc::kill(pid as i32, 0) == 0 }
}
#[cfg(not(unix))]
{
false
}
}
pub fn is_scsh_daemon_pid(pid: u32) -> bool {
#[cfg(unix)]
{
process_args(pid).is_some_and(|args| args.contains("scsh") && args.contains("__daemon-serve"))
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}
#[cfg(unix)]
fn process_args(pid: u32) -> Option<String> {
let output = std::process::Command::new("ps").arg("-p").arg(pid.to_string()).arg("-o").arg("args=").output().ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn read_live_pid(port: u16) -> Option<u32> {
let text = std::fs::read_to_string(pid_file(port)).ok()?;
let pid = text.trim().parse::<u32>().ok()?;
if pid_alive(pid) && is_scsh_daemon_pid(pid) {
Some(pid)
} else {
None
}
}
pub fn base_url(port: u16) -> String {
format!("http://127.0.0.1:{port}")
}
pub fn session_url(port: u16, session_id: &str) -> String {
format!("{}/session/{}", base_url(port), session_id)
}
pub fn absolutize_repo_path(path: &Path) -> String {
let path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")).join(path)
};
std::fs::canonicalize(&path).unwrap_or(path).to_string_lossy().into_owned()
}
#[cfg(unix)]
pub fn daemon_detach_child() -> std::io::Result<()> {
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(std::io::Error::last_os_error());
}
if pid > 0 {
unsafe { libc::_exit(0) };
}
let sid = unsafe { libc::setsid() };
if sid < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(unix)]
mod libc {
#[link(name = "c")]
extern "C" {
pub fn kill(pid: i32, sig: i32) -> i32;
pub fn fork() -> i32;
pub fn setsid() -> i32;
pub fn _exit(code: i32) -> !;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pid_file_names_include_port() {
let p = pid_file(7274);
assert!(p.to_string_lossy().contains("7274"));
assert!(p.to_string_lossy().ends_with(".pid"));
}
#[test]
fn session_url_format() {
let u = session_url(7274, "abcdef");
assert_eq!(u, "http://127.0.0.1:7274/session/abcdef");
}
}