use anyhow::{Context, Result};
use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
pub async fn run_foreground(mut cmd: Command) -> Result<i32> {
let mut child = cmd.spawn().context("spawning server")?;
let status = child.wait().await?;
Ok(status.code().unwrap_or(-1))
}
pub fn spawn_detached(mut cmd: Command, pid_file: &Path) -> Result<u32> {
if let Some(parent) = pid_file.parent() {
std::fs::create_dir_all(parent)?;
}
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.as_std_mut().pre_exec(|| {
nix::unistd::setsid().ok();
Ok(())
});
}
let child = cmd.spawn().context("spawning detached server")?;
let pid = child.id().ok_or_else(|| anyhow::anyhow!("no pid"))?;
write_pid_record(pid_file, pid)?;
Ok(pid)
}
fn write_pid_record(pid_file: &Path, pid: u32) -> Result<()> {
let token = process_token(pid).unwrap_or_default();
let body = if token.is_empty() {
pid.to_string()
} else {
format!("{pid}\n{token}")
};
let tmp = pid_file.with_extension("tmp");
std::fs::write(&tmp, body)?;
std::fs::rename(&tmp, pid_file)?;
Ok(())
}
pub fn read_pid(pid_file: &Path) -> Option<u32> {
let s = std::fs::read_to_string(pid_file).ok()?;
s.lines().next()?.trim().parse().ok()
}
fn read_token(pid_file: &Path) -> Option<String> {
let s = std::fs::read_to_string(pid_file).ok()?;
let mut it = s.lines();
it.next()?; it.next()
.map(|l| l.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn clear_pid(pid_file: &Path) {
std::fs::remove_file(pid_file).ok();
}
pub fn is_running_recorded(pid_file: &Path, pid: u32) -> bool {
if !is_running(pid) {
return false;
}
let saved = read_token(pid_file);
let current = process_token(pid);
match (saved, current) {
(Some(a), Some(b)) => a == b,
(None, _) => true,
(Some(_), None) => true,
}
}
#[cfg(unix)]
pub fn is_running(pid: u32) -> bool {
use nix::sys::signal::kill;
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), None).is_ok()
}
#[cfg(windows)]
pub fn is_running(pid: u32) -> bool {
extern "system" {
fn OpenProcess(dwDesiredAccess: u32, bInheritHandle: i32, dwProcessId: u32) -> isize;
fn CloseHandle(hObject: isize) -> i32;
fn GetExitCodeProcess(hProcess: isize, lpExitCode: *mut u32) -> i32;
}
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const STILL_ACTIVE: u32 = 259;
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle == 0 {
return false;
}
let mut exit_code: u32 = 0;
let ok = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
ok != 0 && exit_code == STILL_ACTIVE
}
}
#[cfg(not(any(unix, windows)))]
pub fn is_running(_pid: u32) -> bool {
false
}
#[cfg(windows)]
fn process_token(pid: u32) -> Option<String> {
extern "system" {
fn OpenProcess(dwDesiredAccess: u32, bInheritHandle: i32, dwProcessId: u32) -> isize;
fn CloseHandle(hObject: isize) -> i32;
fn GetProcessTimes(
hProcess: isize,
lpCreationTime: *mut FILETIME,
lpExitTime: *mut FILETIME,
lpKernelTime: *mut FILETIME,
lpUserTime: *mut FILETIME,
) -> i32;
}
#[repr(C)]
#[derive(Default, Copy, Clone)]
struct FILETIME {
low: u32,
high: u32,
}
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle == 0 {
return None;
}
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
let ok = GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user);
CloseHandle(handle);
if ok == 0 {
return None;
}
Some(format!("{}-{}", creation.high, creation.low))
}
}
#[cfg(unix)]
fn process_token(pid: u32) -> Option<String> {
let content = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let after_comm = content.rsplit_once(')')?.1;
let starttime = after_comm.split_whitespace().nth(19)?;
Some(starttime.to_string())
}
#[cfg(not(any(unix, windows)))]
fn process_token(_pid: u32) -> Option<String> {
None
}
#[cfg(unix)]
pub fn signal_term(pid: u32) -> Result<()> {
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), Signal::SIGTERM)?;
Ok(())
}
#[cfg(unix)]
pub fn signal_kill(pid: u32) -> Result<()> {
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), Signal::SIGKILL)?;
Ok(())
}
#[cfg(windows)]
pub fn signal_term(pid: u32) -> Result<()> {
signal_kill(pid)
}
#[cfg(windows)]
pub fn signal_kill(pid: u32) -> Result<()> {
extern "system" {
fn OpenProcess(dwDesiredAccess: u32, bInheritHandle: i32, dwProcessId: u32) -> isize;
fn TerminateProcess(hProcess: isize, uExitCode: u32) -> i32;
fn CloseHandle(hObject: isize) -> i32;
}
const PROCESS_TERMINATE: u32 = 0x0001;
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
anyhow::ensure!(handle != 0, "OpenProcess failed for pid {pid}");
TerminateProcess(handle, 1);
CloseHandle(handle);
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
pub fn signal_term(_pid: u32) -> Result<()> {
Ok(())
}
#[cfg(not(any(unix, windows)))]
pub fn signal_kill(_pid: u32) -> Result<()> {
Ok(())
}
pub async fn wait_for_exit(pid: u32, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if !is_running(pid) {
return true;
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
!is_running(pid)
}