use std::path::Path;
use crate::args::{BeaconStatusJson, ServiceAction, SimpleStatusJson};
use crate::commands::find_index;
use crate::output::format_uptime;
#[cfg(all(feature = "notify", target_os = "linux"))]
pub(crate) fn find_systemctl() -> std::ffi::OsString {
let usr_bin = std::path::Path::new("/usr/bin/systemctl");
if usr_bin.exists() {
return usr_bin.as_os_str().to_os_string();
}
let bin = std::path::Path::new("/bin/systemctl");
if bin.exists() {
return bin.as_os_str().to_os_string();
}
std::ffi::OsString::from("systemctl")
}
#[cfg(all(feature = "notify", not(target_os = "linux")))]
#[allow(dead_code)]
pub(crate) fn find_systemctl() -> std::ffi::OsString {
std::ffi::OsString::from("systemctl")
}
fn find_ixd() -> std::path::PathBuf {
if let Ok(exe) = std::env::current_exe()
&& let Some(parent) = exe.parent()
{
let candidate = parent.join("ixd");
if candidate.exists() {
return candidate;
}
}
std::path::PathBuf::from("ixd")
}
#[cfg(feature = "notify")]
pub(crate) fn handle_service(action: &ServiceAction) -> ix::error::Result<()> {
match action {
ServiceAction::Status { path, json } => {
handle_service_status(path.as_deref(), *json);
Ok(())
}
ServiceAction::Start { path } => {
let ixd = find_ixd();
let watch_path = path
.clone()
.unwrap_or_else(|| std::path::PathBuf::from("."));
let status = std::process::Command::new(&ixd)
.arg("--daemon")
.arg(&watch_path)
.status()
.map_err(|e| {
ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
})?;
if !status.success() {
return Err(ix::error::Error::Config(
"failed to start ixd daemon".into(),
));
}
println!("ixd daemon started for {}.", watch_path.display());
println!("Run 'ix service status' to verify.");
Ok(())
}
ServiceAction::Stop { path } => {
let ixd = find_ixd();
let stop_path = path
.clone()
.unwrap_or_else(|| std::path::PathBuf::from("."));
let status = std::process::Command::new(&ixd)
.arg("--stop")
.arg(&stop_path)
.status()
.map_err(|e| {
ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
})?;
if !status.success() {
return Err(ix::error::Error::Config("failed to stop ixd daemon".into()));
}
Ok(())
}
ServiceAction::Restart { path } => {
let ixd = find_ixd();
let target_path = path
.clone()
.unwrap_or_else(|| std::path::PathBuf::from("."));
let _ = std::process::Command::new(&ixd)
.arg("--stop")
.arg(&target_path)
.status();
std::thread::sleep(std::time::Duration::from_millis(500));
let status = std::process::Command::new(&ixd)
.arg("--daemon")
.arg(&target_path)
.status()
.map_err(|e| {
ix::error::Error::Config(format!("failed to execute {}: {e}", ixd.display()))
})?;
if !status.success() {
return Err(ix::error::Error::Config(
"failed to restart ixd daemon".into(),
));
}
println!("ixd daemon restarted for {}.", target_path.display());
Ok(())
}
ServiceAction::Install { path } => {
#[cfg(target_os = "linux")]
{
let home = std::env::var("HOME")
.map_err(|_| ix::error::Error::Config("HOME not set".into()))?;
let service_dir = std::path::PathBuf::from(&home).join(".config/systemd/user");
let service_file = service_dir.join("ixd.service");
let watch_path = path.clone().unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(&home))
});
let watch_path_abs = watch_path.canonicalize().unwrap_or(watch_path);
let ixd_path = find_ixd();
let ixd_abs = ixd_path.canonicalize().unwrap_or(ixd_path);
std::fs::create_dir_all(&service_dir)?;
let service_content = format!(
r"[Unit]
Description=ix background daemon
After=network.target
[Service]
ExecStart={} {}
Restart=on-failure
RestartSec=10
StartLimitBurst=3
StartLimitIntervalSec=60
[Install]
WantedBy=default.target
",
ixd_abs.display(),
watch_path_abs.display()
);
std::fs::write(&service_file, service_content)?;
let status = std::process::Command::new(find_systemctl())
.args(["--user", "daemon-reload"])
.status();
match status {
Ok(s) if s.success() => {
println!("ixd service installed at {}", service_file.display());
println!("Watch path: {}", watch_path_abs.display());
println!("Run 'ix service start' to start the daemon.");
}
_ => {
println!("ixd service file written to {}", service_file.display());
println!(
"Note: systemd daemon-reload was not run or systemd is not active on this system."
);
println!("You can run 'ix service start' directly to run ixd natively.");
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
let _ = path;
println!("Systemd installation is only applicable to Linux.");
println!(
"On this platform, run 'ix service start' directly to launch ixd in background."
);
Ok(())
}
}
}
}
#[cfg(not(feature = "notify"))]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn handle_service(action: &ServiceAction) -> ix::error::Result<()> {
if let ServiceAction::Status { path, json } = &action {
handle_service_status(path.as_deref(), *json);
return Ok(());
}
eprintln!("Error: ix service commands require the 'notify' feature.");
eprintln!("Install with: cargo install moeix --features notify");
std::process::exit(1);
}
pub(crate) fn handle_service_status(path: Option<&Path>, json: bool) {
use std::time::{SystemTime, UNIX_EPOCH};
let search_path = path.unwrap_or(Path::new("."));
let beacon_opt = find_index(search_path).and_then(|(_, _, beacon)| beacon);
match beacon_opt {
Some(beacon) if beacon.is_live() => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let uptime = now.saturating_sub(beacon.start_time);
if json {
print_json_status("running", &beacon, Some(uptime));
} else {
println!("ixd daemon is running");
println!(" PID: {}", beacon.pid);
println!(" Uptime: {}", format_uptime(uptime));
println!(" Status: {}", beacon.status);
println!(" Root: {}", beacon.root.display());
if let Some(ref sock) = beacon.socket_path {
println!(" Socket: {}", sock.display());
}
}
}
Some(beacon) => {
#[cfg(unix)]
let is_orphan = {
use nix::sys::signal::kill;
use nix::unistd::Pid;
kill(Pid::from_raw(beacon.pid), None).is_ok()
};
#[cfg(not(unix))]
let is_orphan = false;
if is_orphan {
if json {
let out = SimpleStatusJson {
status: "orphan".to_string(),
stale_pid: Some(beacon.pid),
};
if let Ok(s) = serde_json::to_string(&out) {
println!("{s}");
}
} else {
println!("PID {} is not ixd (orphan beacon)", beacon.pid);
}
} else if json {
let out = SimpleStatusJson {
status: "dead".to_string(),
stale_pid: Some(beacon.pid),
};
if let Ok(s) = serde_json::to_string(&out) {
println!("{s}");
}
} else {
println!(
"ixd daemon is not running (stale beacon from PID {})",
beacon.pid
);
}
}
None => {
if json {
let out = SimpleStatusJson {
status: "not_running".to_string(),
stale_pid: None,
};
if let Ok(s) = serde_json::to_string(&out) {
println!("{s}");
}
} else {
println!("ixd daemon is not running");
}
}
}
}
pub(crate) fn print_json_status(status: &str, beacon: &ix::format::Beacon, uptime: Option<u64>) {
let json = BeaconStatusJson {
status: status.to_string(),
pid: beacon.pid,
uptime_secs: uptime,
daemon_status: beacon.status.clone(),
root: beacon.root.display().to_string(),
socket: beacon.socket_path.as_ref().map(|p| p.display().to_string()),
instance_id: beacon.instance_id,
};
match serde_json::to_string(&json) {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("ix: warning: JSON serialize error: {e}"),
}
}