Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-install-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@resciencelab/agent-world-network": patch
---

Fix install.sh, implement `awn daemon stop` with IPC shutdown + PID fallback
1 change: 1 addition & 0 deletions packages/awn-cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/awn-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dirs = "6"
tracing = "0.1"
thiserror = "2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
libc = "0.2"

[package.metadata.deb]
maintainer = "ReScienceLab <resciencelab@users.noreply.github.com>"
Expand Down
31 changes: 18 additions & 13 deletions packages/awn-cli/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

REPO="ReScienceLab/agent-world-network"
BINARY="awn"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"

info() { printf '\033[1;34m%s\033[0m\n' "$*"; }
error() { printf '\033[1;31merror: %s\033[0m\n' "$*" >&2; exit 1; }
Expand All @@ -29,10 +29,13 @@ detect_target() {
}

get_latest_version() {
curl -sL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' \
local tag
tag="$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep -o '"tag_name": *"[^"]*"' \
| head -1 \
| sed 's/.*"tag_name": *"v\?\([^"]*\)".*/\1/'
| grep -o '"v[^"]*"' \
| tr -d '"v')"
echo "$tag"
}

main() {
Expand All @@ -50,25 +53,27 @@ main() {
local url="https://github.com/${REPO}/releases/download/v${version}/${BINARY}-v${version}-${target}.tar.gz"
local tmp
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
trap "rm -rf '$tmp'" EXIT

info "Downloading awn v${version} for ${target}..."
curl -sL "$url" -o "${tmp}/awn.tar.gz" || error "Download failed. Check that v${version} has a binary for ${target}."
curl -fsSL "$url" -o "${tmp}/awn.tar.gz" || error "Download failed. Check that v${version} has a binary for ${target}."

info "Extracting..."
tar xzf "${tmp}/awn.tar.gz" -C "$tmp"

mkdir -p "$INSTALL_DIR"
info "Installing to ${INSTALL_DIR}..."
if [ -w "$INSTALL_DIR" ]; then
cp "${tmp}/${BINARY}-v${version}-${target}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
chmod +x "${INSTALL_DIR}/${BINARY}"
else
sudo cp "${tmp}/${BINARY}-v${version}-${target}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
sudo chmod +x "${INSTALL_DIR}/${BINARY}"
fi
cp "${tmp}/${BINARY}-v${version}-${target}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
chmod +x "${INSTALL_DIR}/${BINARY}"

info "Done! awn v${version} installed to ${INSTALL_DIR}/${BINARY}"
"${INSTALL_DIR}/${BINARY}" --version

# Hint if INSTALL_DIR is not in PATH
case ":$PATH:" in
*":${INSTALL_DIR}:"*) ;;
*) info "Add ${INSTALL_DIR} to your PATH: export PATH=\"${INSTALL_DIR}:\$PATH\"" ;;
esac
}

main "$@"
47 changes: 42 additions & 5 deletions packages/awn-cli/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use crate::identity::{self, Identity};
use crate::agent_db::{Endpoint, AgentDb, AgentRecord};

const DEFAULT_IPC_PORT: u16 = 8199;
const PORT_FILE: &str = "daemon.port";
const PID_FILE: &str = "daemon.pid";

#[derive(Clone)]
pub struct DaemonState {
Expand Down Expand Up @@ -103,11 +105,32 @@ pub async fn start_daemon(
listen_port,
};

let (shutdown_tx, shutdown_rx) = oneshot::channel();
let (ipc_shutdown_tx, ipc_shutdown_rx) = oneshot::channel::<()>();

let app = Router::new()
.route("/ipc/status", get(handle_status))
.route("/ipc/agents", get(handle_agents))
.route("/ipc/worlds", get(handle_worlds))
.route("/ipc/ping", get(handle_ping))
.route(
"/ipc/shutdown",
post({
let tx = Arc::new(std::sync::Mutex::new(Some(ipc_shutdown_tx)));
move || {
let tx = tx.clone();
async move {
if let Some(tx) = tx.lock().unwrap().take() {
let _ = tx.send(());
}
Json(OkResponse {
ok: true,
message: Some("shutting down".to_string()),
})
}
}
}),
)
.with_state(state);

let addr = SocketAddr::from(([127, 0, 0, 1], ipc_port));
Expand All @@ -116,12 +139,13 @@ pub async fn start_daemon(
.map_err(|e| DaemonError::Bind(e.to_string()))?;
let bound_addr = listener.local_addr().unwrap();

let (shutdown_tx, shutdown_rx) = oneshot::channel();

tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
let _ = shutdown_rx.await;
tokio::select! {
_ = shutdown_rx => {}
_ = ipc_shutdown_rx => {}
}
})
.await
.ok();
Expand Down Expand Up @@ -236,8 +260,6 @@ pub fn default_gateway_url() -> String {
std::env::var("GATEWAY_URL").unwrap_or_else(|_| "https://gateway.agentworlds.ai".to_string())
}

const PORT_FILE: &str = "daemon.port";

pub fn write_port_file(data_dir: &std::path::Path, port: u16) {
let _ = std::fs::create_dir_all(data_dir);
let _ = std::fs::write(data_dir.join(PORT_FILE), port.to_string());
Expand All @@ -253,6 +275,21 @@ pub fn remove_port_file(data_dir: &std::path::Path) {
let _ = std::fs::remove_file(data_dir.join(PORT_FILE));
}

pub fn write_pid_file(data_dir: &std::path::Path) {
let _ = std::fs::create_dir_all(data_dir);
let _ = std::fs::write(data_dir.join(PID_FILE), std::process::id().to_string());
}

pub fn read_pid_file(data_dir: &std::path::Path) -> Option<u32> {
std::fs::read_to_string(data_dir.join(PID_FILE))
.ok()
.and_then(|s| s.trim().parse().ok())
}

pub fn remove_pid_file(data_dir: &std::path::Path) {
let _ = std::fs::remove_file(data_dir.join(PID_FILE));
}

#[derive(Debug, thiserror::Error)]
pub enum DaemonError {
#[error("identity error: {0}")]
Expand Down
76 changes: 56 additions & 20 deletions packages/awn-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ enum DaemonAction {
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let json_output = cli.json;
let cli_ipc_port = cli.ipc_port;

match cli.command {
Commands::Daemon { action } => match action {
Expand All @@ -71,12 +73,13 @@ async fn main() {
} => {
let data_dir = data_dir.unwrap_or_else(daemon::default_data_dir);
let gateway_url = gateway_url.unwrap_or_else(daemon::default_gateway_url);
let ipc_port = cli.ipc_port.unwrap_or_else(|| daemon::ipc_port());
let ipc_port = cli_ipc_port.unwrap_or_else(|| daemon::ipc_port());

match daemon::start_daemon(data_dir.clone(), gateway_url, port, ipc_port).await {
Ok(handle) => {
daemon::write_port_file(&data_dir, handle.addr.port());
if cli.json {
daemon::write_pid_file(&data_dir);
if json_output {
println!(
"{}",
serde_json::json!({
Expand All @@ -90,13 +93,14 @@ async fn main() {
}
tokio::signal::ctrl_c().await.ok();
daemon::remove_port_file(&data_dir);
daemon::remove_pid_file(&data_dir);
handle.shutdown();
if !cli.json {
if !json_output {
eprintln!("Daemon stopped");
}
}
Err(e) => {
if cli.json {
if json_output {
println!("{}", serde_json::json!({"error": e.to_string()}));
} else {
eprintln!("Error: {e}");
Expand All @@ -106,21 +110,53 @@ async fn main() {
}
}
DaemonAction::Stop => {
if cli.json {
println!("{}", serde_json::json!({"error": "daemon stop not yet implemented (use Ctrl+C)"}));
} else {
eprintln!("Error: daemon stop not yet implemented. Use Ctrl+C to stop the daemon, or kill the process.");
let data_dir = daemon::default_data_dir();
let ipc = resolve_ipc_port_raw(cli_ipc_port);
let url = format!("http://127.0.0.1:{ipc}/ipc/shutdown");
let client = reqwest::Client::new();
match client.post(&url).send().await {
Ok(resp) if resp.status().is_success() => {
daemon::remove_port_file(&data_dir);
daemon::remove_pid_file(&data_dir);
if json_output {
println!("{}", serde_json::json!({"ok": true, "message": "daemon stopped"}));
} else {
println!("Daemon stopped.");
}
}
_ => {
// Fallback: try to kill by PID
if let Some(pid) = daemon::read_pid_file(&data_dir) {
unsafe {
if libc::kill(pid as i32, libc::SIGTERM) == 0 {
daemon::remove_port_file(&data_dir);
daemon::remove_pid_file(&data_dir);
if json_output {
println!("{}", serde_json::json!({"ok": true, "message": format!("sent SIGTERM to pid {pid}")}));
} else {
println!("Sent SIGTERM to daemon (pid {pid}).");
}
return;
}
}
}
if json_output {
println!("{}", serde_json::json!({"error": "daemon not running"}));
} else {
eprintln!("Daemon not running.");
}
std::process::exit(1);
}
}
std::process::exit(1);
}
},
Commands::Status => {
let ipc = resolve_ipc_port(&cli);
let ipc = resolve_ipc_port_raw(cli_ipc_port);
let url = format!("http://127.0.0.1:{ipc}/ipc/status");
match reqwest::get(&url).await {
Ok(resp) => {
if let Ok(status) = resp.json::<daemon::StatusResponse>().await {
if cli.json {
if json_output {
println!("{}", serde_json::to_string(&status).unwrap());
} else {
println!("=== AWN Status ===");
Expand All @@ -134,7 +170,7 @@ async fn main() {
}
}
Err(_) => {
if cli.json {
if json_output {
println!("{}", serde_json::json!({"error": "AWN daemon not running. Start with: awn daemon start"}));
} else {
eprintln!("AWN daemon not running. Start with: awn daemon start");
Expand All @@ -144,15 +180,15 @@ async fn main() {
}
}
Commands::Agents { ref capability } => {
let ipc = resolve_ipc_port(&cli);
let ipc = resolve_ipc_port_raw(cli_ipc_port);
let mut url = format!("http://127.0.0.1:{ipc}/ipc/agents");
if let Some(cap) = capability {
url = format!("{url}?capability={}", urlencoding(cap));
}
match reqwest::get(&url).await {
Ok(resp) => {
if let Ok(data) = resp.json::<daemon::AgentsResponse>().await {
if cli.json {
if json_output {
println!("{}", serde_json::to_string(&data).unwrap());
} else if data.agents.is_empty() {
println!("No agents found.");
Expand All @@ -176,7 +212,7 @@ async fn main() {
}
}
Err(_) => {
if cli.json {
if json_output {
println!("{}", serde_json::json!({"error": "AWN daemon not running. Start with: awn daemon start"}));
} else {
eprintln!("AWN daemon not running. Start with: awn daemon start");
Expand All @@ -186,12 +222,12 @@ async fn main() {
}
}
Commands::Worlds => {
let ipc = resolve_ipc_port(&cli);
let ipc = resolve_ipc_port_raw(cli_ipc_port);
let url = format!("http://127.0.0.1:{ipc}/ipc/worlds");
match reqwest::get(&url).await {
Ok(resp) => {
if let Ok(data) = resp.json::<daemon::WorldsResponse>().await {
if cli.json {
if json_output {
println!("{}", serde_json::to_string(&data).unwrap());
} else if data.worlds.is_empty() {
println!("No worlds found.");
Expand All @@ -206,7 +242,7 @@ async fn main() {
}
}
Err(_) => {
if cli.json {
if json_output {
println!("{}", serde_json::json!({"error": "AWN daemon not running. Start with: awn daemon start"}));
} else {
eprintln!("AWN daemon not running. Start with: awn daemon start");
Expand All @@ -218,8 +254,8 @@ async fn main() {
}
}

fn resolve_ipc_port(cli: &Cli) -> u16 {
if let Some(port) = cli.ipc_port {
fn resolve_ipc_port_raw(cli_ipc_port: Option<u16>) -> u16 {
if let Some(port) = cli_ipc_port {
return port;
}
if let Ok(port) = std::env::var("AWN_IPC_PORT").and_then(|s| s.parse().map_err(|_| std::env::VarError::NotPresent)) {
Expand Down
Loading