diff options
| -rw-r--r-- | docs/next/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/cli/server.rs | 8 | ||||
| -rw-r--r-- | src/session.rs | 61 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 20 |
4 files changed, 65 insertions, 25 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index c1b36fa4..27249ac6 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -16,6 +16,7 @@ - Bumped the client/server protocol version to 15 for socket API placement mutation event and response compatibility. ### Fixed +- `herdr server stop` now waits until both server sockets are unreachable before returning, avoiding an immediate first-start failure when restarting right after replacing the binary. - Unix local Herdr clients no longer treat empty bracketed paste as a clipboard-image bridge; `herdr --remote` keeps using it for local-desktop image paste over SSH. (#986) - Plain PageUp/PageDown now reach primary-screen pager apps such as `less -X` and Git diff when they enter application cursor mode, while shell transcripts still use Herdr pane scrollback. (#953) - `prefix+e` scrollback editor panes now open on Windows without trying to run `/bin/sh`; Windows uses `VISUAL`, then `EDITOR`, then `notepad.exe` as the fallback editor. (#914) diff --git a/src/cli/server.rs b/src/cli/server.rs index b0648cde..29775999 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -30,7 +30,13 @@ fn server_stop(args: &[String]) -> std::io::Result<i32> { return Ok(2); } - super::send_ok_request(Method::ServerStop(EmptyParams::default())) + match crate::session::stop_active_server() { + Ok(()) => Ok(0), + Err(err) => { + eprintln!("{err}"); + Ok(1) + } + } } fn server_reload_config(args: &[String]) -> std::io::Result<i32> { diff --git a/src/session.rs b/src/session.rs index 34d202a5..4dfb2658 100644 --- a/src/session.rs +++ b/src/session.rs @@ -233,9 +233,37 @@ pub fn stop_session(name: Option<&str>) -> Result<SessionInfo, String> { stop_session_with_timeout(name, STOP_WAIT_TIMEOUT) } +pub(crate) fn stop_active_server() -> Result<(), String> { + let socket_path = active_api_socket_path(); + let client_socket_path = crate::server::socket_paths::client_socket_path(); + stop_socket_with_timeout( + socket_path.clone(), + vec![socket_path, client_socket_path], + STOP_WAIT_TIMEOUT, + "server", + ) +} + fn stop_session_with_timeout(name: Option<&str>, timeout: Duration) -> Result<SessionInfo, String> { - let deadline = Instant::now() + timeout; let socket_path = api_socket_path_for(name); + let client_socket_path = client_socket_path_for(name); + let label = format!("session {}", name.unwrap_or(DEFAULT_SESSION_NAME)); + stop_socket_with_timeout( + socket_path.clone(), + vec![socket_path, client_socket_path], + timeout, + &label, + )?; + Ok(session_info(name)) +} + +fn stop_socket_with_timeout( + socket_path: PathBuf, + stopped_socket_paths: Vec<PathBuf>, + timeout: Duration, + label: &str, +) -> Result<(), String> { + let deadline = Instant::now() + timeout; let request = serde_json::json!({ "id": "cli:session:stop", "method": "server.stop", @@ -243,8 +271,7 @@ fn stop_session_with_timeout(name: Option<&str>, timeout: Duration) -> Result<Se }); let stream = crate::ipc::connect_local_stream(&socket_path).map_err(|err| { format!( - "session {} is not running or cannot be reached at {}: {err}", - name.unwrap_or(DEFAULT_SESSION_NAME), + "{label} is not running or cannot be reached at {}: {err}", socket_path.display() ) })?; @@ -254,15 +281,19 @@ fn stop_session_with_timeout(name: Option<&str>, timeout: Duration) -> Result<Se return Err(error.to_string()); } } - if !wait_until_stopped_until(&socket_path, deadline) { + if !wait_until_stopped_until(&stopped_socket_paths, deadline) { + let reachable = reachable_socket_paths(&stopped_socket_paths); return Err(format!( - "session {} did not stop within {}ms; socket is still reachable at {}", - name.unwrap_or(DEFAULT_SESSION_NAME), + "{label} did not stop within {}ms; sockets are still reachable at {}", timeout.as_millis(), - socket_path.display() + reachable + .iter() + .map(|path| path.display().to_string()) + .collect::<Vec<_>>() + .join(", ") )); } - Ok(session_info(name)) + Ok(()) } pub fn delete_session(name: &str) -> Result<SessionInfo, String> { @@ -353,14 +384,22 @@ fn is_running_at(socket_path: &Path) -> bool { socket_path.exists() && crate::ipc::connect_local_stream(socket_path).is_ok() } -fn wait_until_stopped_until(socket_path: &Path, deadline: Instant) -> bool { +fn wait_until_stopped_until(socket_paths: &[PathBuf], deadline: Instant) -> bool { while Instant::now() < deadline { - if !is_running_at(socket_path) { + if socket_paths.iter().all(|path| !is_running_at(path)) { return true; } std::thread::sleep(STOP_WAIT_POLL.min(time_until(deadline))); } - !is_running_at(socket_path) + socket_paths.iter().all(|path| !is_running_at(path)) +} + +fn reachable_socket_paths(socket_paths: &[PathBuf]) -> Vec<PathBuf> { + socket_paths + .iter() + .filter(|path| is_running_at(path)) + .cloned() + .collect() } fn time_until(deadline: Instant) -> Duration { diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index c1bf27c0..d3f67780 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -1756,26 +1756,20 @@ fn server_stop_command_shuts_down_running_server() { "server stop should not print stdout: {}", String::from_utf8_lossy(&stopped.stdout) ); - - let pid = herdr.child.process_id(); - let exit_status = herdr.child.wait().unwrap(); - unregister_spawned_herdr_pid(pid); - assert!(exit_status.success(), "server stop should exit cleanly"); - - let deadline = Instant::now() + Duration::from_secs(3); - while Instant::now() < deadline && (socket_path.exists() || client_socket.exists()) { - thread::sleep(Duration::from_millis(25)); - } - assert!( !socket_path.exists() || UnixStream::connect(&socket_path).is_err(), - "api socket should be removed or stale after server stop" + "api socket should be removed or stale before server stop returns" ); assert!( !client_socket.exists() || UnixStream::connect(&client_socket).is_err(), - "client socket should be removed or stale after server stop" + "client socket should be removed or stale before server stop returns" ); + let pid = herdr.child.process_id(); + let exit_status = herdr.child.wait().unwrap(); + unregister_spawned_herdr_pid(pid); + assert!(exit_status.success(), "server stop should exit cleanly"); + cleanup_spawned_herdr(herdr, base); } |
