diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/mod.rs | 101 | ||||
| -rw-r--r-- | src/api/schema.rs | 56 |
2 files changed, 108 insertions, 49 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 1b81e09c..437a616e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,7 +2,6 @@ pub mod schema; use std::fs; use std::io::{BufRead, BufReader, Read, Write}; -use std::os::unix::fs::PermissionsExt; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -28,6 +27,25 @@ const APP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5); const INITIAL_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); const STREAM_WRITE_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) fn request_changes_ui(request: &Request) -> bool { + matches!( + &request.method, + Method::WorkspaceCreate(_) + | Method::WorkspaceFocus(_) + | Method::WorkspaceRename(_) + | Method::WorkspaceClose(_) + | Method::TabCreate(_) + | Method::TabFocus(_) + | Method::TabRename(_) + | Method::TabClose(_) + | Method::PaneSplit(_) + | Method::PaneReportAgent(_) + | Method::PaneClearAgentAuthority(_) + | Method::PaneReleaseAgent(_) + | Method::PaneClose(_) + ) +} + pub struct ApiRequestMessage { pub request: Request, pub respond_to: std::sync::mpsc::Sender<String>, @@ -80,16 +98,6 @@ pub fn socket_path() -> PathBuf { return PathBuf::from(path); } - let socket_name = if crate::config::app_dir_name() == "herdr" { - "herdr.sock" - } else { - "herdr-dev.sock" - }; - - if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") { - return PathBuf::from(dir).join(socket_name); - } - crate::config::config_dir().join("herdr.sock") } @@ -156,47 +164,16 @@ pub fn start_server( } fn prepare_socket_path(path: &Path) -> std::io::Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - if !path.exists() { - return Ok(()); - } - - match UnixStream::connect(path) { - Ok(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::AddrInUse, - format!( - "herdr is already running (socket busy at {})", - path.display() - ), - )); - } - Err(err) - if matches!( - err.kind(), - std::io::ErrorKind::ConnectionRefused - | std::io::ErrorKind::NotFound - | std::io::ErrorKind::TimedOut - ) => {} - Err(err) => return Err(err), - } - - if let Err(err) = fs::remove_file(path) { - if err.kind() != std::io::ErrorKind::NotFound { - return Err(err); - } - } - - Ok(()) + crate::ipc::prepare_socket_path(path, |path| { + format!( + "herdr is already running (socket busy at {})", + path.display() + ) + }) } fn restrict_socket_permissions(path: &Path) -> std::io::Result<()> { - let mut permissions = fs::metadata(path)?.permissions(); - permissions.set_mode(SOCKET_PERMISSION_MODE); - fs::set_permissions(path, permissions) + crate::ipc::restrict_socket_permissions(path, SOCKET_PERMISSION_MODE) } fn handle_connection( @@ -947,6 +924,13 @@ fn error_response_json(id: String, code: &str, message: String) -> String { #[cfg(test)] mod tests { use super::*; + use std::os::unix::fs::PermissionsExt; + use std::sync::{Mutex, OnceLock}; + + fn env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } fn unique_test_path(name: &str) -> PathBuf { let nanos = std::time::SystemTime::now() @@ -965,6 +949,7 @@ mod tests { #[test] fn socket_path_prefers_explicit_env_override() { + let _guard = env_lock().lock().unwrap(); let unique = format!("/tmp/herdr-test-{}.sock", std::process::id()); std::env::set_var(SOCKET_PATH_ENV_VAR, &unique); assert_eq!(socket_path(), PathBuf::from(&unique)); @@ -972,6 +957,24 @@ mod tests { } #[test] + fn socket_path_defaults_to_config_dir_even_when_xdg_runtime_dir_is_set() { + let _guard = env_lock().lock().unwrap(); + let config_home = unique_test_path("socket-default-config-home"); + let runtime_dir = unique_test_path("socket-default-runtime"); + std::env::remove_var(SOCKET_PATH_ENV_VAR); + std::env::set_var("XDG_CONFIG_HOME", &config_home); + std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir); + + let expected = config_home + .join(crate::config::app_dir_name()) + .join("herdr.sock"); + assert_eq!(socket_path(), expected); + + std::env::remove_var("XDG_CONFIG_HOME"); + std::env::remove_var("XDG_RUNTIME_DIR"); + } + + #[test] fn restrict_socket_permissions_sets_user_only_mode() { let dir = unique_test_path("socket-perms"); fs::create_dir_all(&dir).unwrap(); diff --git a/src/api/schema.rs b/src/api/schema.rs index 38393141..6cb81c12 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -12,6 +12,8 @@ pub struct Request { pub enum Method { #[serde(rename = "ping")] Ping(PingParams), + #[serde(rename = "server.stop")] + ServerStop(EmptyParams), #[serde(rename = "workspace.create")] WorkspaceCreate(WorkspaceCreateParams), #[serde(rename = "workspace.list")] @@ -64,6 +66,10 @@ pub enum Method { EventsWait(EventsWaitParams), #[serde(rename = "pane.wait_for_output")] PaneWaitForOutput(PaneWaitForOutputParams), + #[serde(rename = "integration.install")] + IntegrationInstall(IntegrationInstallParams), + #[serde(rename = "integration.uninstall")] + IntegrationUninstall(IntegrationUninstallParams), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -286,6 +292,25 @@ pub struct PaneWaitForOutputParams { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IntegrationInstallParams { + pub target: IntegrationTarget, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IntegrationUninstallParams { + pub target: IntegrationTarget, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IntegrationTarget { + Pi, + Claude, + Codex, + Opencode, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum OutputMatch { Substring { value: String }, @@ -442,6 +467,14 @@ pub enum ResponseResult { matched_line: Option<String>, read: PaneReadResult, }, + IntegrationInstall { + target: IntegrationTarget, + details: IntegrationInstallResult, + }, + IntegrationUninstall { + target: IntegrationTarget, + details: IntegrationUninstallResult, + }, Ok {}, } @@ -494,6 +527,16 @@ pub struct PaneReadResult { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IntegrationInstallResult { + pub messages: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IntegrationUninstallResult { + pub messages: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EventEnvelope { pub event: EventKind, pub data: EventData, @@ -710,6 +753,19 @@ mod tests { } #[test] + fn request_round_trips_for_server_stop() { + let request = Request { + id: "req_stop".into(), + method: Method::ServerStop(EmptyParams::default()), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "server.stop"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); + } + + #[test] fn unknown_method_is_rejected() { let json = r#"{"id":"req_1","method":"nope","params":{}}"#; let err = serde_json::from_str::<Request>(json) |
