#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::process::Command;
pub fn notify_ready() -> anyhow::Result<()> {
if let Ok(socket_path) = std::env::var("NOTIFY_SOCKET") {
use std::os::unix::net::UnixDatagram;
let socket = UnixDatagram::unbound()?;
socket.send_to(b"READY=1", &socket_path)?;
tracing::info!("Sent sd_notify READY=1");
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn unit_file_path() -> PathBuf {
PathBuf::from("/etc/systemd/system/koi.service")
}
#[cfg(target_os = "linux")]
pub fn install_bin_path() -> PathBuf {
PathBuf::from("/usr/local/bin/koi")
}
#[cfg(target_os = "linux")]
const SERVICE_NAME: &str = "koi";
#[cfg(target_os = "linux")]
pub fn install() -> anyhow::Result<()> {
check_root("install")?;
let exe_path = std::env::current_exe()?;
let install_path = install_bin_path();
let unit_path = unit_file_path();
println!("Installing Koi service...");
println!(" Binary: {}", exe_path.display());
let was_active = systemctl_check("is-active");
if was_active || systemctl_check("is-enabled") {
println!(" Existing service found, updating...");
if was_active {
print!(" Stopping service...");
let _ = Command::new("systemctl")
.args(["stop", SERVICE_NAME])
.output();
println!(" done.");
}
}
print!(" Copying to {}...", install_path.display());
std::fs::copy(&exe_path, &install_path)?;
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(&install_path, perms)?;
}
println!(" done.");
let unit_contents = generate_unit_file(&install_path);
print!(" Writing {}...", unit_path.display());
std::fs::write(&unit_path, unit_contents)?;
println!(" done.");
print!(" Reloading systemd...");
let reload = Command::new("systemctl").args(["daemon-reload"]).output();
match reload {
Ok(o) if o.status.success() => println!(" done."),
Ok(o) => println!(" warning: {}", String::from_utf8_lossy(&o.stderr).trim()),
Err(e) => println!(" warning: {e}"),
}
match Command::new("systemctl")
.args(["enable", SERVICE_NAME])
.output()
{
Ok(o) if o.status.success() => println!(" Service enabled (start on boot)"),
Ok(o) => println!(
" Warning: could not enable service: {}",
String::from_utf8_lossy(&o.stderr).trim()
),
Err(e) => println!(" Warning: could not enable service: {e}"),
}
match Command::new("systemctl")
.args(["start", SERVICE_NAME])
.output()
{
Ok(o) if o.status.success() => {
if was_active {
println!(" Service restarted");
} else {
println!(" Service started");
}
}
Ok(o) => println!(
" Warning: could not start service: {}",
String::from_utf8_lossy(&o.stderr).trim()
),
Err(e) => println!(" Warning: could not start service: {e}"),
}
println!();
println!("Koi service installed.");
println!(" \u{b0}\u{2027} \u{1f41f} \u{b7}\u{ff61} the local waters are calm");
println!();
println!(" Modules enabled:");
println!(" mDNS service discovery (active)");
println!(" DNS static + certmesh entries (ready)");
println!(" CertMesh certificate mesh CA (ready \u{2014} run certmesh create)");
println!(" Health endpoint health checks (ready)");
println!(" Proxy TLS reverse proxy (ready)");
println!();
println!(" Logs: journalctl -u {SERVICE_NAME}");
println!(" Config: systemctl edit {SERVICE_NAME}");
println!(" Use `koi status` to see module state.");
Ok(())
}
#[cfg(target_os = "linux")]
pub fn uninstall() -> anyhow::Result<()> {
let unit_path = unit_file_path();
let install_path = install_bin_path();
if !unit_path.exists() {
println!("Koi is not installed as a systemd service. Nothing to uninstall.");
return Ok(());
}
check_root("uninstall")?;
println!("Uninstalling Koi service...");
if let Some(bc) = koi_config::breadcrumb::read_breadcrumb() {
let client = crate::client::KoiClient::with_token(&bc.endpoint, &bc.token);
if client.shutdown().is_ok() {
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
if systemctl_check("is-active") {
print!(" Stopping service...");
let _ = Command::new("systemctl")
.args(["stop", SERVICE_NAME])
.output();
println!(" done.");
}
match Command::new("systemctl")
.args(["disable", SERVICE_NAME])
.output()
{
Ok(o) if o.status.success() => println!(" Service disabled"),
_ => {}
}
print!(" Removing {}...", unit_path.display());
match std::fs::remove_file(&unit_path) {
Ok(()) => println!(" done."),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => println!(" already removed."),
Err(e) => println!(" warning: {e}"),
}
print!(" Reloading systemd...");
match Command::new("systemctl").args(["daemon-reload"]).output() {
Ok(o) if o.status.success() => println!(" done."),
_ => println!(" warning."),
}
koi_config::breadcrumb::delete_breadcrumb();
if install_path.exists() {
println!(" Binary preserved at: {}", install_path.display());
}
println!();
println!("Koi service uninstalled.");
Ok(())
}
#[cfg(target_os = "linux")]
use super::check_root;
#[cfg(target_os = "linux")]
fn systemctl_check(query: &str) -> bool {
Command::new("systemctl")
.args([query, SERVICE_NAME])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
fn generate_unit_file(bin_path: &std::path::Path) -> String {
format!(
"\
[Unit]
Description=Koi Network Toolkit
Documentation=https://github.com/sylin-org/koi
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart={} --daemon
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
",
bin_path.display()
)
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
fn unit_paths_are_expected() {
assert!(unit_file_path().ends_with("koi.service"));
assert!(install_bin_path().ends_with("/usr/local/bin/koi"));
}
#[test]
fn unit_file_contains_execstart_and_notify() {
let unit = generate_unit_file(&std::path::PathBuf::from("/usr/local/bin/koi"));
assert!(unit.contains("ExecStart=/usr/local/bin/koi --daemon"));
assert!(unit.contains("Type=notify"));
}
}