use anyhow::Result;
use super::style::{banner, dim, green, kv, red, yellow};
use crate::gateway;
use rsclaw_cli::GatewayCommand;
use rsclaw_config as config;
use rsclaw_platform::detect_memory_tier;
const VERSION: &str = match option_env!("RSCLAW_BUILD_VERSION") {
Some(v) => v,
None => "dev",
};
fn spawn_gateway_bg() -> Result<std::process::Child> {
spawn_gateway_bg_pub()
}
pub fn spawn_gateway_bg_pub() -> Result<std::process::Child> {
let exe = std::env::current_exe()?;
let mut cmd = std::process::Command::new(&exe);
if let Ok(v) = std::env::var("RSCLAW_BASE_DIR") {
cmd.env("RSCLAW_BASE_DIR", v);
}
if let Ok(v) = std::env::var("RSCLAW_PORT") {
cmd.env("RSCLAW_PORT", v);
}
let log_path = rsclaw_config::loader::log_file();
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let null_path = if cfg!(windows) { "NUL" } else { "/dev/null" };
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.unwrap_or_else(|_| std::fs::File::open(null_path).expect("failed to open null device"));
let log_file2 = log_file
.try_clone()
.unwrap_or_else(|_| std::fs::File::open(null_path).expect("failed to open null device"));
if std::env::var("RUST_LOG").is_err() {
cmd.env("RUST_LOG", "rsclaw=info");
}
cmd.arg("gateway")
.arg("run")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log_file))
.stderr(std::process::Stdio::from(log_file2));
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
Ok(cmd.spawn()?)
}
pub async fn cmd_gateway(sub: GatewayCommand) -> Result<()> {
match sub {
GatewayCommand::Run(_args) => {
if rsclaw_migrate::check_needs_setup() {
return Ok(());
}
let config = std::sync::Arc::new(config::load_quiet()?);
let port = config.gateway.port;
let port_in_use = std::net::TcpListener::bind(format!("127.0.0.1:{port}")).is_err();
if port_in_use {
eprintln!(" [!] Port {port} already in use. Another gateway instance is running.");
eprintln!(" [!] Exiting cleanly to avoid conflict.");
std::process::exit(0);
}
let bind = match config.gateway.bind {
rsclaw_config::schema::BindMode::Auto
| rsclaw_config::schema::BindMode::Lan
| rsclaw_config::schema::BindMode::All => "0.0.0.0",
rsclaw_config::schema::BindMode::Loopback => "loopback",
rsclaw_config::schema::BindMode::Custom => "custom",
rsclaw_config::schema::BindMode::Tailnet => "tailnet",
};
let pid = std::process::id();
banner(&format!("rsclaw gateway {VERSION}"));
kv("Port:", &format!("{port} | Bind: {bind}"));
kv("PID:", &format!("{pid}"));
println!();
let tier = detect_memory_tier();
gateway::startup::start_gateway(config, tier).await
}
GatewayCommand::Start => {
if rsclaw_migrate::check_needs_setup() {
return Ok(());
}
banner(&format!("rsclaw gateway {VERSION}"));
if let Some(pid) = gateway_read_pid()
&& process_alive(pid)
{
println!(" {} Gateway already running (pid {pid})", yellow("[!]"));
return Ok(());
}
if service_installed() {
println!(
" {} Service detected, starting via service manager...",
dim("[..]")
);
if try_service_start() {
std::thread::sleep(std::time::Duration::from_secs(2));
if let Some(pid) = gateway_read_pid() {
if process_alive(pid) {
println!(
" {} Gateway started (via service, pid {pid})",
green("[ok]")
);
kv("URL:", &detect_url());
println!();
return Ok(());
}
}
eprintln!(
" {} Service loaded but gateway not running, falling back to direct start",
yellow("[!]")
);
} else {
eprintln!(
" {} Service start failed, falling back to direct start",
yellow("[!]")
);
}
}
let child = spawn_gateway_bg()?;
let pid = child.id();
println!(" {} Gateway started", green("[ok]"));
kv("PID:", &format!("{pid}"));
kv("URL:", &detect_url());
println!();
Ok(())
}
GatewayCommand::Stop => {
let pid_display = gateway_read_pid()
.map(|p| format!(" (pid {p})"))
.unwrap_or_default();
match gateway_signal_stop() {
Ok(()) => println!(" {} Gateway stopped{pid_display}", green("[ok]")),
Err(e) => println!(" {} {e}", yellow("[!]")),
}
Ok(())
}
GatewayCommand::Restart => {
banner(&format!("rsclaw gateway {VERSION}"));
let health = health_url();
let restart = restart_url();
let strategy = RestartStrategy::choose(gateway_health_reachable(&health).await);
match strategy {
RestartStrategy::HttpGraceful => {
println!(" {} Requesting graceful restart...", dim("[..]"));
request_graceful_restart(&restart).await?;
println!(" {} Waiting for gateway health...", dim("[..]"));
wait_for_gateway_health(&health).await?;
println!(" {} Gateway restarted", green("[ok]"));
kv("URL:", &detect_url());
println!();
Ok(())
}
RestartStrategy::DirectStopStart => {
match gateway_signal_stop() {
Ok(()) => println!(" {} Stopped old gateway", dim("[..]")),
Err(error) => match restart_fallback_after_stop_error(error) {
RestartFallbackDecision::StartFresh => println!(
" {} No running gateway found, starting fresh",
dim("[..]")
),
RestartFallbackDecision::Abort { reason } => anyhow::bail!(reason),
},
}
if service_installed() {
if try_service_start() {
wait_for_gateway_health(&health).await?;
println!(" {} Gateway restarted via service", green("[ok]"));
kv("URL:", &detect_url());
println!();
return Ok(());
}
eprintln!(
" {} Service start failed, falling back to direct start",
yellow("[!]")
);
}
let child = spawn_gateway_bg()?;
let pid = child.id();
wait_for_gateway_health(&health).await?;
println!(" {} Gateway restarted", green("[ok]"));
kv("PID:", &format!("{pid}"));
kv("URL:", &detect_url());
println!();
Ok(())
}
}
}
GatewayCommand::Status => gateway_print_status().await,
GatewayCommand::Health => {
let config = config::load_quiet().ok();
let port = config.map(|c| c.gateway.port).unwrap_or(18888);
let url = format!("http://127.0.0.1:{port}/api/v1/health");
match reqwest::Client::new().get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
println!(" [ok] Healthy -- {url}");
}
Ok(resp) => {
println!(" [!!] Unhealthy -- {} {url}", resp.status());
}
Err(_) => {
println!(" [!!] Unreachable -- {url}");
}
}
Ok(())
}
GatewayCommand::Install => cmd_gateway_install().await,
GatewayCommand::Uninstall => cmd_gateway_uninstall().await,
GatewayCommand::Probe => {
let config = std::sync::Arc::new(config::load_quiet()?);
let port = config.gateway.port;
let url = format!("http://127.0.0.1:{port}/api/v1/health");
let resp = reqwest::Client::new()
.get(&url)
.send()
.await
.map_err(|e| anyhow::anyhow!("gateway unreachable at {url}: {e}"))?;
println!(" {} -- {url}", resp.status());
Ok(())
}
GatewayCommand::Discover => {
println!("Scanning local network for rsclaw/openclaw gateways...");
println!("(discovery uses mDNS/broadcast -- not yet implemented)");
println!("Try: http://127.0.0.1:{}", detect_port());
Ok(())
}
GatewayCommand::UsageCost => {
let config = config::load_quiet().ok();
let port = config.as_ref().map(|c| c.gateway.port).unwrap_or(18888);
let auth_token = config
.and_then(|c| c.gateway.auth_token)
.unwrap_or_default();
let url = format!("http://127.0.0.1:{port}/api/v1/usage");
let mut req = reqwest::Client::new().get(&url);
if !auth_token.is_empty() {
req = req.bearer_auth(&auth_token);
}
match req.send().await {
Ok(resp) if resp.status().is_success() => {
let body: serde_json::Value = resp.json().await.unwrap_or_default();
println!("{}", serde_json::to_string_pretty(&body)?);
}
Ok(resp) => {
println!("usage endpoint returned: {}", resp.status());
}
Err(_) => {
println!("gateway not reachable at port {port}");
}
}
Ok(())
}
GatewayCommand::Call { method, args } => {
let config = std::sync::Arc::new(config::load_quiet()?);
let port = config.gateway.port;
let auth_token = config.gateway.auth_token.clone().unwrap_or_default();
let url = format!("http://127.0.0.1:{port}/api/v1/{method}");
let body: serde_json::Value = if args.is_empty() {
serde_json::Value::Object(Default::default())
} else {
serde_json::from_str(&args.join(" "))
.unwrap_or(serde_json::Value::String(args.join(" ")))
};
let mut req = reqwest::Client::new().post(&url).json(&body);
if !auth_token.is_empty() {
req = req.bearer_auth(&auth_token);
}
let resp = req
.send()
.await
.map_err(|e| anyhow::anyhow!("gateway unreachable at {url}: {e}"))?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
println!("{status} {text}");
Ok(())
}
}
}
pub fn gateway_pid_file() -> std::path::PathBuf {
config::loader::pid_file()
}
fn gateway_read_pid() -> Option<u32> {
std::fs::read_to_string(gateway_pid_file())
.ok()?
.trim()
.parse::<u32>()
.ok()
}
fn process_alive(pid: u32) -> bool {
rsclaw_platform::process_alive(pid)
}
fn find_gateway_pid() -> Option<u32> {
let port = detect_port();
let my_pid = std::process::id();
#[cfg(unix)]
{
let output = std::process::Command::new("lsof")
.args(["-ti", &format!(":{port}"), "-sTCP:LISTEN"])
.output()
.ok();
if let Some(output) = output {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if let Ok(pid) = line.trim().parse::<u32>() {
if pid != my_pid && process_alive(pid) {
return Some(pid);
}
}
}
}
for pattern in &["rsclaw gateway", "rsclaw"] {
let output = std::process::Command::new("pgrep")
.args(["-f", pattern])
.output()
.ok();
if let Some(output) = output {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if let Ok(pid) = line.trim().parse::<u32>() {
if pid != my_pid && process_alive(pid) {
return Some(pid);
}
}
}
}
}
}
#[cfg(windows)]
{
#[allow(unused_mut)]
let mut ns = std::process::Command::new("netstat");
ns.args(["-ano"]);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
ns.creation_flags(0x08000000);
}
let output = ns.output()
.ok();
if let Some(output) = output {
let text = String::from_utf8_lossy(&output.stdout);
let port_str = format!(":{port}");
for line in text.lines() {
if line.contains(&port_str) && line.contains("LISTENING") {
if let Some(pid_str) = line.split_whitespace().last() {
if let Ok(pid) = pid_str.parse::<u32>() {
if pid != my_pid && process_alive(pid) {
return Some(pid);
}
}
}
}
}
}
}
None
}
fn detect_port() -> u16 {
config::load_quiet()
.ok()
.map(|c| c.gateway.port)
.unwrap_or(18888)
}
pub const STOP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
const STOP_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
pub const START_HEALTH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(150);
const START_HEALTH_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StopWaitOutcome {
pid: u32,
stopped: bool,
}
impl StopWaitOutcome {
pub fn from_alive_samples<I>(pid: u32, samples: I) -> Self
where
I: IntoIterator<Item = bool>,
{
let stopped = samples.into_iter().any(|alive| !alive);
Self { pid, stopped }
}
pub fn is_stopped(&self) -> bool {
self.stopped
}
pub fn error_message(&self) -> Option<String> {
if self.stopped {
None
} else {
Some(format!(
"gateway process {} did not stop before timeout",
self.pid
))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HealthWaitOutcome {
Healthy,
Timeout { url: String },
}
pub fn health_wait_result<I>(probe_results: I, url: &str) -> HealthWaitOutcome
where
I: IntoIterator<Item = bool>,
{
if probe_results.into_iter().any(|ok| ok) {
HealthWaitOutcome::Healthy
} else {
HealthWaitOutcome::Timeout {
url: url.to_owned(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestartStrategy {
HttpGraceful,
DirectStopStart,
}
impl RestartStrategy {
pub fn choose(gateway_reachable: bool) -> Self {
if gateway_reachable {
Self::HttpGraceful
} else {
Self::DirectStopStart
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RestartFallbackDecision {
StartFresh,
Abort { reason: String },
}
const GATEWAY_NOT_RUNNING_MSG: &str = "gateway is not running";
pub fn restart_fallback_after_stop_result(
stop_result: std::result::Result<(), &str>,
) -> RestartFallbackDecision {
match stop_result {
Ok(()) => RestartFallbackDecision::StartFresh,
Err(message)
if message.starts_with(GATEWAY_NOT_RUNNING_MSG)
|| message.contains("is not running") =>
{
RestartFallbackDecision::StartFresh
}
Err(message) => RestartFallbackDecision::Abort {
reason: message.to_owned(),
},
}
}
fn restart_fallback_after_stop_error(error: anyhow::Error) -> RestartFallbackDecision {
let message = error.to_string();
restart_fallback_after_stop_result(Err(&message))
}
fn health_url() -> String {
let port = detect_port();
format!("http://127.0.0.1:{port}/api/v1/health")
}
fn restart_url() -> String {
let port = detect_port();
format!("http://127.0.0.1:{port}/api/v1/restart")
}
fn gateway_auth_token() -> String {
config::load_quiet()
.ok()
.and_then(|c| c.gateway.auth_token)
.unwrap_or_default()
}
async fn gateway_health_reachable(url: &str) -> bool {
reqwest::Client::new()
.get(url)
.send()
.await
.map(|resp| resp.status().is_success())
.unwrap_or(false)
}
async fn request_graceful_restart(url: &str) -> Result<()> {
let token = gateway_auth_token();
let mut req = reqwest::Client::new().post(url);
if !token.is_empty() {
req = req.bearer_auth(token);
}
let resp = req
.send()
.await
.map_err(|e| anyhow::anyhow!("failed to request graceful restart at {url}: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("graceful restart request failed: {} {url}", resp.status());
}
Ok(())
}
pub fn should_remove_pid_after_stop(outcome: &StopWaitOutcome) -> bool {
outcome.is_stopped()
}
async fn wait_for_gateway_health(url: &str) -> Result<()> {
let client = reqwest::Client::new();
let deadline = std::time::Instant::now() + START_HEALTH_TIMEOUT;
let mut samples = Vec::new();
loop {
let ok = match client.get(url).send().await {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
};
samples.push(ok);
match health_wait_result(samples.iter().copied(), url) {
HealthWaitOutcome::Healthy => return Ok(()),
HealthWaitOutcome::Timeout { .. } => {}
}
if std::time::Instant::now() >= deadline {
anyhow::bail!(
"gateway did not become healthy at {url}; check {}",
rsclaw_config::loader::log_file().display()
);
}
tokio::time::sleep(START_HEALTH_POLL_INTERVAL).await;
}
}
fn wait_for_process_exit(pid: u32) -> StopWaitOutcome {
let deadline = std::time::Instant::now() + STOP_TIMEOUT;
loop {
if !process_alive(pid) {
return StopWaitOutcome { pid, stopped: true };
}
if std::time::Instant::now() >= deadline {
return StopWaitOutcome {
pid,
stopped: false,
};
}
std::thread::sleep(STOP_POLL_INTERVAL);
}
}
fn detect_url() -> String {
let cfg = config::load_quiet().ok();
let port = cfg.as_ref().map(|c| c.gateway.port).unwrap_or(18888);
let bind = cfg
.as_ref()
.and_then(|c| c.gateway.bind_address.as_deref())
.unwrap_or("127.0.0.1");
let display_host = if bind == "0.0.0.0" || bind == "::" {
"127.0.0.1"
} else {
bind
};
format!("http://{display_host}:{port}")
}
pub fn gateway_signal_stop() -> Result<()> {
if try_service_stop() {
if let Some(pid) = gateway_read_pid().or_else(find_gateway_pid) {
let outcome = wait_for_process_exit(pid);
if should_remove_pid_after_stop(&outcome) {
let _ = std::fs::remove_file(gateway_pid_file());
return Ok(());
}
anyhow::bail!(
outcome
.error_message()
.expect("timeout has an error message")
);
}
let _ = std::fs::remove_file(gateway_pid_file());
return Ok(());
}
let pid = gateway_read_pid()
.or_else(find_gateway_pid)
.ok_or_else(|| {
anyhow::anyhow!("gateway is not running (no PID file and no matching process)")
})?;
if !process_alive(pid) {
let _ = std::fs::remove_file(gateway_pid_file());
anyhow::bail!("gateway process {pid} is not running");
}
rsclaw_platform::process_terminate(pid)?;
let outcome = wait_for_process_exit(pid);
if should_remove_pid_after_stop(&outcome) {
let _ = std::fs::remove_file(gateway_pid_file());
Ok(())
} else {
anyhow::bail!(
outcome
.error_message()
.expect("timeout has an error message")
);
}
}
fn service_installed() -> bool {
#[cfg(target_os = "macos")]
{
if let Some(home) = dirs_next::home_dir() {
let plist = home.join("Library/LaunchAgents/ai.rsclaw.gateway.plist");
if plist.exists() {
return true;
}
}
}
#[cfg(target_os = "linux")]
{
if let Some(home) = dirs_next::home_dir() {
let unit = home.join(".config/systemd/user/rsclaw-gateway.service");
if unit.exists() {
return true;
}
}
let sys_unit = std::path::Path::new("/etc/systemd/system/rsclaw-gateway.service");
if sys_unit.exists() {
return true;
}
}
#[cfg(target_os = "windows")]
{
#[allow(unused_mut)]
let mut sc_cmd = std::process::Command::new("sc");
sc_cmd.args(["query", "rsclaw"]);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
sc_cmd.creation_flags(0x08000000);
}
if let Ok(o) = sc_cmd.output()
{
if String::from_utf8_lossy(&o.stdout).contains("SERVICE_NAME") {
return true;
}
}
}
false
}
fn try_service_start() -> bool {
#[cfg(target_os = "macos")]
{
if let Some(home) = dirs_next::home_dir() {
let plist = home.join("Library/LaunchAgents/ai.rsclaw.gateway.plist");
if plist.exists() {
let status = std::process::Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist)
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
}
}
#[cfg(target_os = "linux")]
{
if let Some(home) = dirs_next::home_dir() {
let unit = home.join(".config/systemd/user/rsclaw-gateway.service");
if unit.exists() {
let status = std::process::Command::new("systemctl")
.args(["--user", "start", "rsclaw-gateway"])
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
}
let sys_unit = std::path::Path::new("/etc/systemd/system/rsclaw-gateway.service");
if sys_unit.exists() {
let status = std::process::Command::new("systemctl")
.args(["start", "rsclaw-gateway"])
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
}
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
let status = std::process::Command::new("sc")
.args(["start", "rsclaw"])
.creation_flags(0x08000000)
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
#[allow(unreachable_code)]
false
}
fn try_service_stop() -> bool {
#[cfg(target_os = "macos")]
{
let plist =
dirs_next::home_dir().map(|h| h.join("Library/LaunchAgents/ai.rsclaw.gateway.plist"));
if let Some(ref path) = plist {
if path.exists() {
let status = std::process::Command::new("launchctl")
.args(["unload"])
.arg(path)
.status();
if let Ok(s) = status {
if s.success() {
return true;
}
}
}
}
}
#[cfg(target_os = "linux")]
{
let is_active = std::process::Command::new("systemctl")
.args(["--user", "is-active", "rsclaw-gateway"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if is_active {
let status = std::process::Command::new("systemctl")
.args(["--user", "stop", "rsclaw-gateway"])
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
let is_active = std::process::Command::new("systemctl")
.args(["is-active", "rsclaw-gateway"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if is_active {
let status = std::process::Command::new("systemctl")
.args(["stop", "rsclaw-gateway"])
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
}
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
let is_active = std::process::Command::new("sc")
.args(["query", "rsclaw"])
.creation_flags(0x08000000)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("RUNNING"))
.unwrap_or(false);
if is_active {
let status = std::process::Command::new("sc")
.args(["stop", "rsclaw"])
.creation_flags(0x08000000)
.status();
return status.map(|s| s.success()).unwrap_or(false);
}
}
false
}
pub async fn gateway_print_status() -> Result<()> {
let port = detect_port();
let base = config::loader::base_dir();
banner(&format!("rsclaw gateway {VERSION}"));
kv("Base dir:", &format!("{}", base.display()));
kv("Port:", &format!("{port}"));
match gateway_read_pid() {
Some(pid) if process_alive(pid) => {
kv("Status:", &green(&format!("running (pid {pid})")));
kv("URL:", &format!("http://127.0.0.1:{port}"));
let url = format!("http://127.0.0.1:{port}/api/v1/status");
let auth_token = config::load()
.ok()
.and_then(|c| c.gateway.auth_token.clone())
.unwrap_or_default();
let mut req = reqwest::Client::new().get(&url);
if !auth_token.is_empty() {
req = req.bearer_auth(&auth_token);
}
if let Ok(resp) = req.send().await
&& let Ok(body) = resp.json::<serde_json::Value>().await
{
if let Some(v) = body.get("version").and_then(|v| v.as_str()) {
kv("Version:", v);
}
if let Some(a) = body.get("agents").and_then(|v| v.as_u64()) {
kv("Agents:", &format!("{a}"));
}
}
}
Some(pid) => {
let _ = std::fs::remove_file(gateway_pid_file());
kv("Status:", &red(&format!("stopped (stale pid {pid})")));
}
None => {
kv("Status:", &red("stopped"));
}
}
println!();
Ok(())
}
#[cfg(target_os = "macos")]
async fn cmd_gateway_install() -> Result<()> {
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("cannot determine home dir"))?;
let binary = std::env::current_exe()?;
let plist_dir = home.join("Library/LaunchAgents");
std::fs::create_dir_all(&plist_dir)?;
let plist_path = plist_dir.join("ai.rsclaw.gateway.plist");
let log_path = rsclaw_config::loader::log_file();
let plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.rsclaw.gateway</string>
<key>ProgramArguments</key>
<array>
<string>{binary}</string>
<string>gateway</string>
<string>run</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
</dict>
</plist>
"#,
binary = binary.display(),
log = log_path.display(),
);
std::fs::write(&plist_path, &plist)?;
println!(" [+] {}", plist_path.display());
let status = std::process::Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist_path)
.status()?;
if status.success() {
println!(" [ok] Service installed -- starts on login, restarts on crash");
} else {
eprintln!(" [!!] launchctl load failed (exit {})", status);
}
Ok(())
}
#[cfg(target_os = "macos")]
async fn cmd_gateway_uninstall() -> Result<()> {
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("cannot determine home dir"))?;
let plist_path = home.join("Library/LaunchAgents/ai.rsclaw.gateway.plist");
let status = std::process::Command::new("launchctl")
.args(["unload", "-w"])
.arg(&plist_path)
.status()?;
if !status.success() {
eprintln!(" [!] launchctl unload failed (may not have been loaded)");
}
if plist_path.exists() {
std::fs::remove_file(&plist_path)?;
}
println!(" [ok] Service uninstalled");
Ok(())
}
#[cfg(target_os = "linux")]
async fn cmd_gateway_install() -> Result<()> {
let binary = std::env::current_exe()?;
let user = std::env::var("USER").unwrap_or_else(|_| "root".to_owned());
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("cannot determine home dir"))?;
let log_path = rsclaw_config::loader::log_file();
let unit = format!(
"[Unit]\n\
Description=rsclaw AI gateway\n\
After=network.target\n\
\n\
[Service]\n\
Type=simple\n\
User={user}\n\
ExecStart={binary} gateway run\n\
Restart=on-failure\n\
RestartSec=5\n\
StandardOutput=append:{log}\n\
StandardError=append:{log}\n\
\n\
[Install]\n\
WantedBy=default.target\n",
binary = binary.display(),
log = log_path.display(),
);
let unit_dir = home.join(".config/systemd/user");
std::fs::create_dir_all(&unit_dir)?;
let unit_path = unit_dir.join("rsclaw-gateway.service");
std::fs::write(&unit_path, &unit)?;
println!(" [+] {}", unit_path.display());
for cmd in [
vec!["systemctl", "--user", "daemon-reload"],
vec!["systemctl", "--user", "enable", "--now", "rsclaw-gateway"],
] {
let status = std::process::Command::new(cmd[0])
.args(&cmd[1..])
.status()?;
if !status.success() {
eprintln!(" [!!] systemctl {} failed", cmd[1..].join(" "));
}
}
println!(" [ok] Service installed and started");
Ok(())
}
#[cfg(target_os = "linux")]
async fn cmd_gateway_uninstall() -> Result<()> {
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("cannot determine home dir"))?;
let unit_path = home.join(".config/systemd/user/rsclaw-gateway.service");
for cmd in [
vec!["systemctl", "--user", "disable", "--now", "rsclaw-gateway"],
vec!["systemctl", "--user", "daemon-reload"],
] {
let _ = std::process::Command::new(cmd[0]).args(&cmd[1..]).status();
}
if unit_path.exists() {
std::fs::remove_file(&unit_path)?;
}
println!(" [ok] Service uninstalled");
Ok(())
}
#[cfg(target_os = "windows")]
async fn cmd_gateway_install() -> Result<()> {
let binary = std::env::current_exe()?;
let binary_str = binary.to_string_lossy();
let bin_path = format!("\"{}\" gateway run", binary_str);
use std::os::windows::process::CommandExt;
let status = std::process::Command::new("sc")
.args([
"create",
"rsclaw",
"binPath=",
&bin_path,
"start=",
"auto",
"DisplayName=",
"RsClaw AI Gateway",
])
.creation_flags(0x08000000)
.status()?;
if !status.success() {
eprintln!(" [!] sc create failed. Try running as Administrator.");
return Ok(());
}
println!(" [+] Service registered: rsclaw");
let _ = std::process::Command::new("sc")
.args(["start", "rsclaw"])
.creation_flags(0x08000000)
.status();
println!(" [ok] Service installed and started");
Ok(())
}
#[cfg(target_os = "windows")]
async fn cmd_gateway_uninstall() -> Result<()> {
use std::os::windows::process::CommandExt;
let _ = std::process::Command::new("sc")
.args(["stop", "rsclaw"])
.creation_flags(0x08000000)
.status();
std::thread::sleep(std::time::Duration::from_secs(2));
let status = std::process::Command::new("sc")
.args(["delete", "rsclaw"])
.creation_flags(0x08000000)
.status()?;
if !status.success() {
eprintln!(" [!] sc delete failed. Try running as Administrator.");
} else {
println!(" [ok] Service uninstalled");
}
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
async fn cmd_gateway_install() -> Result<()> {
println!(" [!] Gateway install is not supported on this platform");
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
async fn cmd_gateway_uninstall() -> Result<()> {
println!(" [!] Gateway uninstall is not supported on this platform");
Ok(())
}