aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/cli/server.rs8
-rw-r--r--src/session.rs61
-rw-r--r--tests/cli_wrapper.rs20
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);
}