T32: on Windows herdr's client is sought among the herdr.exe processes at or under the session's shell, none under another herdr, whose server and panes are its own; they are read when that list changes, and a server or a command beside the client is passed by, where round 1 found the server taken for the client
This commit is contained in:
5 files changed
+120
-63
No files matched your search
+6
-15
@@ -15,11 +15,11 @@ use super::view::{TerminalEvent, TerminalView};
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(unix)]
|
||||
use unix::{HERDR, Pid, client, connect, home, leader, running};
|
||||
use unix::{Program, client, connect, home, leader, running};
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
#[cfg(windows)]
|
||||
use windows::{HERDR, Pid, client, connect, home, leader, running};
|
||||
use windows::{Program, client, connect, home, leader, running};
|
||||
|
||||
/// How often the session's foreground program is checked.
|
||||
const CHECK: Duration = Duration::from_millis(500);
|
||||
@@ -36,7 +36,7 @@ type Invocation = (Vec<String>, HashMap<String, OsString>);
|
||||
/// Follows the pane herdr's server has focused while herdr's client runs in the session.
|
||||
pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
cx.spawn(async move |view, cx| {
|
||||
let mut seen: Option<(Pid, Option<PathBuf>)> = None;
|
||||
let mut seen: Option<Program> = None;
|
||||
let (mut socket, mut shown, mut retry, mut failed) = (None, None, false, String::new());
|
||||
loop {
|
||||
cx.background_executor().timer(CHECK).await;
|
||||
@@ -63,11 +63,6 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
}
|
||||
};
|
||||
if program != seen {
|
||||
if let Some((process, None)) = &program {
|
||||
log::debug!(
|
||||
"terminal {pid}: process {process}, another user's or gone, does not read"
|
||||
);
|
||||
}
|
||||
seen = program.clone();
|
||||
shown = None;
|
||||
retry = true;
|
||||
@@ -78,16 +73,12 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
}
|
||||
if retry {
|
||||
let found = match program {
|
||||
Some((process, Some(exe)))
|
||||
if exe
|
||||
.file_name()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(HERDR)) =>
|
||||
{
|
||||
Some(program) => {
|
||||
cx.background_executor()
|
||||
.spawn(async move { client(process) })
|
||||
.spawn(async move { client(program) })
|
||||
.await
|
||||
}
|
||||
_ => Ok(None),
|
||||
None => Ok(None),
|
||||
};
|
||||
retry = found.is_err();
|
||||
socket = found.unwrap_or_else(|error| {
|
||||
|
||||
+50
-12
@@ -147,6 +147,18 @@ fn a_process_reads_with_its_path_arguments_variables_and_folder() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn only_herdr_s_program_is_read() {
|
||||
let pid = std::process::id() as i32;
|
||||
assert_eq!(
|
||||
client((pid, None)).expect("a program that does not read"),
|
||||
None
|
||||
);
|
||||
let shell = client((pid, Some("/bin/sh".into())));
|
||||
assert_eq!(shell.expect("another program"), None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn an_updated_herdr_still_names_itself() {
|
||||
@@ -198,38 +210,64 @@ fn a_windows_client_finds_its_server_in_app_data() {
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn herdr_is_sought_nearest_under_the_shell() {
|
||||
use super::windows::nearest;
|
||||
fn herdr_is_sought_at_and_under_the_shell_not_under_herdr() {
|
||||
use super::windows::herdrs;
|
||||
let processes = [
|
||||
(100, 50, "pwsh.exe"),
|
||||
(104, 100, "cmd.exe"),
|
||||
(108, 104, "HERDR.EXE"),
|
||||
(112, 100, "herdr.exe"),
|
||||
(116, 112, "herdr.exe"),
|
||||
(120, 100, "herdr.exe"),
|
||||
(200, 300, "a.exe"),
|
||||
(300, 200, "b.exe"),
|
||||
]
|
||||
.map(|(pid, parent, name)| (pid, parent, name.to_owned()));
|
||||
let pid = |root| nearest(&processes, root, HERDR).map(|(pid, ..)| *pid);
|
||||
assert_eq!(pid(100), Some(112));
|
||||
assert_eq!(pid(104), Some(108));
|
||||
assert_eq!(pid(112), None);
|
||||
assert_eq!(pid(200), None);
|
||||
// 116 is the server 112 started; 120 one started beside it.
|
||||
assert_eq!(herdrs(&processes, 100), [112, 120, 108]);
|
||||
assert_eq!(herdrs(&processes, 112), [112]);
|
||||
assert!(herdrs(&processes, 200).is_empty());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn a_server_or_a_command_beside_herdr_s_client_is_passed_by() {
|
||||
use super::windows::pick;
|
||||
let data = env(&[("APPDATA", r"C:\D")]);
|
||||
let read = |words: &str| -> io::Result<Invocation> { Ok((args(words), data.clone())) };
|
||||
let gone = || -> io::Result<Invocation> { Err(io::ErrorKind::NotFound.into()) };
|
||||
let socket = Some(PathBuf::from(r"C:\D\herdr\herdr.sock"));
|
||||
let server = "herdr.exe server";
|
||||
let client = [(200, read(server)), (100, read("herdr.exe"))];
|
||||
assert_eq!(pick(client).expect("a client"), socket);
|
||||
let command = [(200, read(server)), (300, read("herdr.exe api snapshot"))];
|
||||
assert_eq!(pick(command).expect("no client"), None);
|
||||
let unread = [(300, gone()), (100, read("herdr.exe"))];
|
||||
assert_eq!(pick(unread).expect("a client"), socket);
|
||||
let error = pick([(300, gone()), (200, read(server))]).expect_err("an unread process");
|
||||
assert!(format!("{error:#}").contains("process 300"), "{error:#}");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn a_windows_process_reads_with_its_name_arguments_and_variables() {
|
||||
use super::windows::launch;
|
||||
use super::windows::{launch, processes};
|
||||
let pid = std::process::id();
|
||||
let exe = std::env::current_exe().expect("this test's path");
|
||||
let name = exe.file_name().expect("this test's name");
|
||||
let (process, read) = running(pid).expect("the process list");
|
||||
assert_eq!(process, pid);
|
||||
assert!(read.is_some_and(|read| read.as_os_str().eq_ignore_ascii_case(name)));
|
||||
let listed = processes().expect("the process list");
|
||||
assert!(
|
||||
listed
|
||||
.iter()
|
||||
.any(|(id, _, read)| *id == pid && name.eq_ignore_ascii_case(read)),
|
||||
"process {pid} is not listed under its name"
|
||||
);
|
||||
assert!(running(pid).expect("the process list").is_empty());
|
||||
let (args, env) = launch(pid).expect("its arguments");
|
||||
assert_eq!(args, std::env::args().collect::<Vec<_>>());
|
||||
// Windows names it Path; the reader folds names to upper case.
|
||||
assert_eq!(env.get("PATH"), std::env::var_os("PATH").as_ref());
|
||||
let path = std::env::var_os("PATH");
|
||||
assert!(env.get("PATH") == path.as_ref(), "PATH reads otherwise");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
+16
-4
@@ -13,21 +13,33 @@ use crate::term::Pty;
|
||||
|
||||
pub(super) type Pid = i32;
|
||||
|
||||
/// The foreground's group and its leader's executable: an exec keeps the group and swaps the program.
|
||||
pub(super) type Program = (Pid, Option<PathBuf>);
|
||||
|
||||
/// herdr's executable.
|
||||
pub(super) const HERDR: &str = "herdr";
|
||||
const HERDR: &str = "herdr";
|
||||
|
||||
/// The process group leading the session's foreground.
|
||||
pub(super) fn leader(pty: &Pty) -> io::Result<Pid> {
|
||||
pty.foreground()
|
||||
}
|
||||
|
||||
/// The group's leader and its executable: an exec keeps the group and swaps the program.
|
||||
pub(super) fn running(group: Pid) -> io::Result<(Pid, Option<PathBuf>)> {
|
||||
pub(super) fn running(group: Pid) -> io::Result<Program> {
|
||||
Ok((group, path(group).ok()))
|
||||
}
|
||||
|
||||
/// The socket of the herdr server whose terminal client leads process group `group`, if it is one.
|
||||
pub(super) fn client(group: Pid) -> anyhow::Result<Option<PathBuf>> {
|
||||
pub(super) fn client((group, exe): Program) -> anyhow::Result<Option<PathBuf>> {
|
||||
let Some(exe) = exe else {
|
||||
log::debug!("herdr: process {group}, another user's or gone, does not read");
|
||||
return Ok(None);
|
||||
};
|
||||
if !exe
|
||||
.file_name()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(HERDR))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let (args, env) =
|
||||
launch(group).with_context(|| format!("herdr: process {group} does not read"))?;
|
||||
let Some(socket) = follows(&args, &env)? else {
|
||||
|
||||
+47
-31
@@ -30,46 +30,43 @@ use crate::term::Pty;
|
||||
|
||||
pub(super) type Pid = u32;
|
||||
|
||||
/// The herdr processes at or under the session's shell, nearest first.
|
||||
pub(super) type Program = Vec<Pid>;
|
||||
|
||||
/// herdr's executable.
|
||||
pub(super) const HERDR: &str = "herdr.exe";
|
||||
const HERDR: &str = "herdr.exe";
|
||||
|
||||
/// The session's shell: Windows has no foreground group, so herdr is sought under it.
|
||||
pub(super) fn leader(pty: &Pty) -> io::Result<Pid> {
|
||||
Ok(pty.pid)
|
||||
}
|
||||
|
||||
/// The herdr process nearest under `shell`, else `shell`; each with its executable's name.
|
||||
pub(super) fn running(shell: Pid) -> io::Result<(Pid, Option<PathBuf>)> {
|
||||
let processes = processes()?;
|
||||
let found = nearest(&processes, shell, HERDR)
|
||||
.or_else(|| processes.iter().find(|(pid, ..)| *pid == shell));
|
||||
Ok(found.map_or((shell, None), |(pid, _, name)| {
|
||||
(*pid, Some(PathBuf::from(name)))
|
||||
}))
|
||||
pub(super) fn running(shell: Pid) -> io::Result<Program> {
|
||||
Ok(herdrs(&processes()?, shell))
|
||||
}
|
||||
|
||||
/// The process nearest under `root` that runs `name`, breadth first.
|
||||
pub(super) fn nearest<'a>(
|
||||
processes: &'a [(Pid, Pid, String)],
|
||||
root: Pid,
|
||||
name: &str,
|
||||
) -> Option<&'a (Pid, Pid, String)> {
|
||||
let mut under = vec![root];
|
||||
let mut at = 0;
|
||||
while let Some(&parent) = under.get(at) {
|
||||
/// The herdr processes at or under `root`, breadth first; what a herdr starts, its server among them, is its own.
|
||||
pub(super) fn herdrs(processes: &[(Pid, Pid, String)], root: Pid) -> Vec<Pid> {
|
||||
let herdr = |pid: Pid| {
|
||||
processes
|
||||
.iter()
|
||||
.any(|(id, _, name)| *id == pid && name.eq_ignore_ascii_case(HERDR))
|
||||
};
|
||||
let (mut under, mut found, mut at) = (vec![root], Vec::new(), 0);
|
||||
while let Some(&pid) = under.get(at) {
|
||||
at += 1;
|
||||
for process in processes.iter().filter(|(_, of, _)| *of == parent) {
|
||||
if herdr(pid) {
|
||||
found.push(pid);
|
||||
continue;
|
||||
}
|
||||
for (child, ..) in processes.iter().filter(|(_, of, _)| *of == pid) {
|
||||
// A parent id outlives its process, so a stale one may lead back up.
|
||||
if under.contains(&process.0) {
|
||||
continue;
|
||||
if !under.contains(child) {
|
||||
under.push(*child);
|
||||
}
|
||||
if process.2.eq_ignore_ascii_case(name) {
|
||||
return Some(process);
|
||||
}
|
||||
under.push(process.0);
|
||||
}
|
||||
}
|
||||
None
|
||||
found
|
||||
}
|
||||
|
||||
/// Every process: its id, its parent's, and its executable's name.
|
||||
@@ -107,11 +104,30 @@ pub(super) fn processes() -> io::Result<Vec<(Pid, Pid, String)>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The socket of the herdr server whose terminal client is process `pid`, if it is one.
|
||||
pub(super) fn client(pid: Pid) -> anyhow::Result<Option<PathBuf>> {
|
||||
let (args, env) = launch(pid).with_context(|| format!("herdr: process {pid} does not read"))?;
|
||||
// herdr names its pipe after the socket's path as written, relative or not.
|
||||
follows(&args, &env)
|
||||
/// The socket of the herdr server whose terminal client is among `herdrs`; a server or a command is passed by.
|
||||
pub(super) fn client(herdrs: Program) -> anyhow::Result<Option<PathBuf>> {
|
||||
pick(herdrs.into_iter().map(|pid| (pid, launch(pid))))
|
||||
}
|
||||
|
||||
/// The first terminal client's socket in `reads`; with none, a failed read is the error, so it is tried again.
|
||||
pub(super) fn pick(
|
||||
reads: impl IntoIterator<Item = (Pid, io::Result<Invocation>)>,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
let mut unread = None;
|
||||
for (pid, read) in reads {
|
||||
match read.with_context(|| format!("herdr: process {pid} does not read")) {
|
||||
// herdr names its pipe after the socket's path as written, relative or not.
|
||||
Ok((args, env)) => {
|
||||
if let Some(socket) = follows(&args, &env)? {
|
||||
return Ok(Some(socket));
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
unread.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
unread.map_or(Ok(None), Err)
|
||||
}
|
||||
|
||||
/// The arguments and variables of process `pid`; names fold to upper case, as Windows matches them.
|
||||
|
||||
Reference in new issue
Block a user