T29: the foreground is its group and the executable its leader runs, so exec herdr from a shell is followed and a program that did not read is tried again; a relative herdr socket lies in the client's folder; on Linux an executable an update replaced still names itself herdr
CI / platforms (ubuntu-24.04) (push) Successful in 1m16s
CI / platforms (windows-latest) (push) Successful in 1m26s
CI / macos (push) Successful in 12m16s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-16 03:19:13 -04:00
1 parent 3e30f95543
commit 2af45cbfb9
4 files changed
+119 -28

No files matched your search

+95 -25
View File
@@ -28,37 +28,60 @@ type Invocation = (Vec<String>, HashMap<String, OsString>);
/// Follows the pane herdr's server has focused while herdr's client runs in the foreground.
pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
cx.spawn(async move |view, cx| {
let (mut group, mut socket, mut shown, mut failed) = (None, None, None, String::new());
let mut seen: Option<(i32, Option<PathBuf>)> = None;
let (mut socket, mut shown, mut retry, mut failed) = (None, None, false, String::new());
loop {
cx.background_executor().timer(CHECK).await;
let Ok((pid, now)) = view.update(cx, |view, _| (view.pty.pid, view.pty.foreground()))
let Ok((pid, group)) = view.update(cx, |view, _| (view.pty.pid, view.pty.foreground()))
else {
break;
};
let now = match now {
Ok(now) => Some(now),
let group = match group {
Ok(group) => Some(group),
Err(error) => {
if group.is_some() {
if seen.is_some() {
log::debug!("terminal {pid}: its foreground does not read: {error}");
}
None
}
};
if now != group {
group = now;
// An exec keeps the group and swaps the program, so the two name the foreground.
let program = match group {
Some(group) => {
let exe = cx
.background_executor()
.spawn(async move { path(group).ok() })
.await;
Some((group, exe))
}
None => None,
};
if program != seen {
if let Some((group, None)) = &program {
log::debug!(
"terminal {pid}: process {group}, another user's or gone, does not read"
);
}
seen = program.clone();
shown = None;
retry = true;
failed.clear();
if socket.take().is_some() {
log::info!("terminal {pid}: herdr left the foreground");
}
let found = match now {
Some(now) => {
}
if retry {
let found = match program {
Some((group, Some(exe)))
if exe.file_name().is_some_and(|name| name == "herdr") =>
{
cx.background_executor()
.spawn(async move { client(now) })
.spawn(async move { client(group) })
.await
}
None => Ok(None),
_ => Ok(None),
};
retry = found.is_err();
socket = found.unwrap_or_else(|error| {
failure(&mut failed, pid, &error);
None
@@ -130,22 +153,20 @@ fn starting(error: &anyhow::Error) -> bool {
})
}
/// The socket of the herdr server whose terminal client leads process group `group`, if one does.
/// The socket of the herdr server whose terminal client leads process group `group`, if it is one.
fn client(group: i32) -> anyhow::Result<Option<PathBuf>> {
let path = match path(group) {
Ok(path) => path,
// Another user's program, such as sudo, or one already gone.
Err(error) => {
log::debug!("terminal: process {group} does not read: {error}");
return Ok(None);
}
};
if path.file_name().is_none_or(|name| name != "herdr") {
return Ok(None);
}
let (args, env) =
launch(group).with_context(|| format!("herdr: process {group} does not read"))?;
follows(&args, &env)
let Some(socket) = follows(&args, &env)? else {
return Ok(None);
};
if socket.is_absolute() {
return Ok(Some(socket));
}
// herdr reads a relative socket from its client's folder.
let folder =
cwd(group).with_context(|| format!("herdr: process {group}'s folder does not read"))?;
Ok(Some(folder.join(socket)))
}
/// The socket a herdr process with `args` and `env` shows, if it is herdr's terminal client.
@@ -305,7 +326,17 @@ pub(super) fn focused(reply: &str) -> anyhow::Result<Option<PathBuf>> {
#[cfg(target_os = "linux")]
fn path(pid: i32) -> io::Result<PathBuf> {
std::fs::read_link(format!("/proc/{pid}/exe"))
std::fs::read_link(format!("/proc/{pid}/exe")).map(installed)
}
/// The path of an executable the kernel marks ` (deleted)`, as an update that replaced it leaves it.
#[cfg(target_os = "linux")]
pub(super) fn installed(path: PathBuf) -> PathBuf {
use std::os::unix::ffi::OsStrExt;
match path.as_os_str().as_bytes().strip_suffix(b" (deleted)") {
Some(kept) => PathBuf::from(OsStr::from_bytes(kept)),
None => path,
}
}
#[cfg(target_os = "linux")]
@@ -317,6 +348,11 @@ fn launch(pid: i32) -> io::Result<Invocation> {
Ok((args, env))
}
#[cfg(target_os = "linux")]
fn cwd(pid: i32) -> io::Result<PathBuf> {
std::fs::read_link(format!("/proc/{pid}/cwd"))
}
/// Each NUL-ended string in `bytes`.
#[cfg(target_os = "linux")]
fn strings(bytes: &[u8]) -> impl Iterator<Item = &[u8]> {
@@ -373,6 +409,40 @@ fn launch(pid: i32) -> io::Result<Invocation> {
procargs(&buffer)
}
#[cfg(target_os = "macos")]
fn cwd(pid: i32) -> io::Result<PathBuf> {
// SAFETY: the struct is plain C data, valid zeroed.
let mut info: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_vnodepathinfo>() as libc::c_int;
// SAFETY: `info` holds the size given.
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDVNODEPATHINFO,
0,
(&raw mut info).cast(),
size,
)
};
if read <= 0 {
return Err(io::Error::last_os_error());
}
if read != size {
return Err(io::Error::other(format!(
"proc_pidinfo read {read} of {size} bytes"
)));
}
let path = info
.pvi_cdir
.vip_path
.iter()
.flatten()
.map(|byte| *byte as u8);
Ok(PathBuf::from(OsString::from_vec(
path.take_while(|byte| *byte != 0).collect(),
)))
}
/// `KERN_PROCARGS2`'s layout: argc, the program's path, NUL padding, the arguments, the variables.
#[cfg(target_os = "macos")]
pub(super) fn procargs(buffer: &[u8]) -> io::Result<Invocation> {
+22 -1
View File
@@ -38,6 +38,8 @@ fn a_client_finds_its_server_as_herdr_does() {
let socket = env(&[("HOME", "/h"), ("HERDR_SOCKET_PATH", "/s.sock")]);
assert_eq!(found("herdr", &socket), PathBuf::from("/s.sock"));
assert_eq!(found("herdr --session work", &socket), work);
let relative = env(&[("HOME", "/h"), ("HERDR_SOCKET_PATH", "herdr.sock")]);
assert_eq!(found("herdr", &relative), PathBuf::from("herdr.sock"));
let named = env(&[("HOME", "/h"), ("HERDR_SESSION", "work")]);
assert_eq!(found("herdr", &named), work);
assert!(follows(&args("herdr"), &env(&[])).is_err());
@@ -120,7 +122,7 @@ fn herdr_answers_over_its_socket() {
/// macOS hides the variables of its own programs, such as `/bin/sleep`, so the test reads itself.
#[test]
fn a_process_reads_with_its_path_arguments_and_variables() {
fn a_process_reads_with_its_path_arguments_variables_and_folder() {
let pid = std::process::id() as i32;
let read = path(pid).expect("its path");
let (args, env) = launch(pid).expect("its arguments");
@@ -128,6 +130,25 @@ fn a_process_reads_with_its_path_arguments_and_variables() {
assert_eq!(fs::canonicalize(read).ok(), fs::canonicalize(exe).ok());
assert_eq!(args, std::env::args().collect::<Vec<_>>());
assert_eq!(env.get("HOME"), std::env::var_os("HOME").as_ref());
let folder = std::env::current_dir().expect("this test's folder");
assert_eq!(
fs::canonicalize(cwd(pid).expect("its folder")).ok(),
fs::canonicalize(folder).ok()
);
}
#[cfg(target_os = "linux")]
#[test]
fn an_updated_herdr_still_names_itself() {
let deleted = PathBuf::from("/home/u/.local/bin/herdr (deleted)");
assert_eq!(
installed(deleted),
PathBuf::from("/home/u/.local/bin/herdr")
);
assert_eq!(
installed("/usr/bin/herdr".into()),
PathBuf::from("/usr/bin/herdr")
);
}
#[cfg(target_os = "macos")]