T32: on Windows the right column follows herdr's focused pane: the herdr.exe nearest under the session's shell, its arguments and variables read through sysinfo, its folder found as herdr finds it there, herdr's named pipe asked as herdr names it; on every system a herdr answer that lands after the shell reported a folder is dropped
This commit is contained in:
13 files changed
+626
-218
No files matched your search
+38
-203
@@ -2,7 +2,6 @@ use std::{
|
||||
collections::HashMap,
|
||||
ffi::{OsStr, OsString},
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
os::unix::{ffi::OsStringExt, net::UnixStream},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -13,6 +12,15 @@ use serde::Deserialize;
|
||||
|
||||
use super::view::{TerminalEvent, TerminalView};
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(unix)]
|
||||
use unix::{HERDR, Pid, client, connect, home, leader, running};
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
#[cfg(windows)]
|
||||
use windows::{HERDR, Pid, client, connect, home, leader, running};
|
||||
|
||||
/// How often the session's foreground program is checked.
|
||||
const CHECK: Duration = Duration::from_millis(500);
|
||||
|
||||
@@ -22,22 +30,31 @@ const WAIT: Duration = Duration::from_secs(1);
|
||||
/// The socket request for herdr's session: its focus and its panes' folders.
|
||||
const SNAPSHOT: &[u8] = b"{\"id\":\"elyterminal\",\"method\":\"session.snapshot\",\"params\":{}}\n";
|
||||
|
||||
/// A program's arguments, and its variables as the kernel holds them.
|
||||
/// A program's arguments, and its variables as the system holds them.
|
||||
type Invocation = (Vec<String>, HashMap<String, OsString>);
|
||||
|
||||
/// Follows the pane herdr's server has focused while herdr's client runs in the foreground.
|
||||
/// 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<(i32, Option<PathBuf>)> = None;
|
||||
let mut seen: Option<(Pid, 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, group)) = view.update(cx, |view, _| (view.pty.pid, view.pty.foreground()))
|
||||
else {
|
||||
let Ok((pid, lead, reports)) = view.update(cx, |view, _| {
|
||||
(view.pty.pid, leader(&view.pty), view.reports)
|
||||
}) else {
|
||||
break;
|
||||
};
|
||||
let group = match group {
|
||||
Ok(group) => Some(group),
|
||||
let program = match lead {
|
||||
Ok(lead) => {
|
||||
cx.background_executor()
|
||||
.spawn(async move { running(lead) })
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
let program = match program {
|
||||
Ok(program) => Some(program),
|
||||
Err(error) => {
|
||||
if seen.is_some() {
|
||||
log::debug!("terminal {pid}: its foreground does not read: {error}");
|
||||
@@ -45,21 +62,10 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
None
|
||||
}
|
||||
};
|
||||
// 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 {
|
||||
if let Some((process, None)) = &program {
|
||||
log::debug!(
|
||||
"terminal {pid}: process {group}, another user's or gone, does not read"
|
||||
"terminal {pid}: process {process}, another user's or gone, does not read"
|
||||
);
|
||||
}
|
||||
seen = program.clone();
|
||||
@@ -72,11 +78,13 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
}
|
||||
if retry {
|
||||
let found = match program {
|
||||
Some((group, Some(exe)))
|
||||
if exe.file_name().is_some_and(|name| name == "herdr") =>
|
||||
Some((process, Some(exe)))
|
||||
if exe
|
||||
.file_name()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(HERDR)) =>
|
||||
{
|
||||
cx.background_executor()
|
||||
.spawn(async move { client(group) })
|
||||
.spawn(async move { client(process) })
|
||||
.await
|
||||
}
|
||||
_ => Ok(None),
|
||||
@@ -104,9 +112,9 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
Ok(Some(folder)) if shown.as_ref() != Some(&folder) => {
|
||||
failed.clear();
|
||||
let report = folder.clone();
|
||||
// herdr may have left while it answered; the shell's own report then stands.
|
||||
// The shell reports only once herdr has left; its folder then stands.
|
||||
let Ok(still) = view.update(cx, |view, cx| {
|
||||
let still = view.pty.foreground().ok() == group;
|
||||
let still = view.reports == reports;
|
||||
if still {
|
||||
cx.emit(TerminalEvent::Folder(report));
|
||||
}
|
||||
@@ -153,22 +161,6 @@ fn starting(error: &anyhow::Error) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 (args, env) =
|
||||
launch(group).with_context(|| format!("herdr: process {group} does not read"))?;
|
||||
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.
|
||||
pub(super) fn follows(
|
||||
args: &[String],
|
||||
@@ -209,10 +201,9 @@ fn attaches(args: &[String]) -> bool {
|
||||
|
||||
/// The socket herdr 0.9's client finds: a named session, `HERDR_SOCKET_PATH`, `HERDR_SESSION`, the default.
|
||||
fn socket(args: &[String], env: &HashMap<String, OsString>) -> anyhow::Result<PathBuf> {
|
||||
let dir = match (env.get("XDG_CONFIG_HOME"), env.get("HOME")) {
|
||||
(Some(config), _) => PathBuf::from(config).join("herdr"),
|
||||
(None, Some(home)) => PathBuf::from(home).join(".config/herdr"),
|
||||
(None, None) => anyhow::bail!("herdr: its client has no HOME"),
|
||||
let dir = match env.get("XDG_CONFIG_HOME") {
|
||||
Some(config) => PathBuf::from(config).join("herdr"),
|
||||
None => home(env)?,
|
||||
};
|
||||
let named = |name: &OsStr| {
|
||||
if name == "default" {
|
||||
@@ -258,9 +249,7 @@ fn session(args: &[String]) -> Option<String> {
|
||||
/// The folder of the pane herdr's server has focused; none while it has none or knows none.
|
||||
fn folder(socket: &Path) -> anyhow::Result<Option<PathBuf>> {
|
||||
let ask = || -> io::Result<String> {
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_read_timeout(Some(WAIT))?;
|
||||
stream.set_write_timeout(Some(WAIT))?;
|
||||
let mut stream = connect(socket)?;
|
||||
stream.write_all(SNAPSHOT)?;
|
||||
let mut reply = String::new();
|
||||
BufReader::new(stream).read_line(&mut reply)?;
|
||||
@@ -324,159 +313,5 @@ pub(super) fn focused(reply: &str) -> anyhow::Result<Option<PathBuf>> {
|
||||
Ok(pane.cwd)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn path(pid: i32) -> io::Result<PathBuf> {
|
||||
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")]
|
||||
fn launch(pid: i32) -> io::Result<Invocation> {
|
||||
let read = |name: &str| std::fs::read(format!("/proc/{pid}/{name}"));
|
||||
let (args, env) = (read("cmdline")?, read("environ")?);
|
||||
let args = strings(&args).map(lossy).collect();
|
||||
let env = strings(&env).filter_map(variable).collect();
|
||||
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]> {
|
||||
bytes
|
||||
.strip_suffix(b"\0")
|
||||
.unwrap_or(bytes)
|
||||
.split(|byte| *byte == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn path(pid: i32) -> io::Result<PathBuf> {
|
||||
let mut buffer = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
|
||||
// SAFETY: the buffer holds the size given.
|
||||
let length =
|
||||
unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) };
|
||||
if length <= 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
buffer.truncate(length as usize);
|
||||
Ok(PathBuf::from(OsString::from_vec(buffer)))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch(pid: i32) -> io::Result<Invocation> {
|
||||
let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid];
|
||||
let mut ask = |buffer: &mut Vec<u8>| {
|
||||
let mut size = buffer.len();
|
||||
let into = if buffer.is_empty() {
|
||||
std::ptr::null_mut()
|
||||
} else {
|
||||
buffer.as_mut_ptr().cast()
|
||||
};
|
||||
// SAFETY: `into` holds `size` bytes, or is null to ask for the size.
|
||||
let done = unsafe {
|
||||
libc::sysctl(
|
||||
name.as_mut_ptr(),
|
||||
3,
|
||||
into,
|
||||
&mut size,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if done != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(size)
|
||||
};
|
||||
let mut buffer = Vec::new();
|
||||
let size = ask(&mut buffer)?;
|
||||
buffer.resize(size, 0);
|
||||
let size = ask(&mut buffer)?;
|
||||
buffer.truncate(size);
|
||||
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> {
|
||||
let invalid =
|
||||
|what: &str| io::Error::new(io::ErrorKind::InvalidData, format!("KERN_PROCARGS2 {what}"));
|
||||
let (count, rest) = buffer
|
||||
.split_first_chunk::<4>()
|
||||
.ok_or_else(|| invalid("holds no argument count"))?;
|
||||
let count = usize::try_from(i32::from_ne_bytes(*count))
|
||||
.map_err(|_| invalid("holds a negative argument count"))?;
|
||||
let mut parts = rest
|
||||
.split(|byte| *byte == 0)
|
||||
.skip(1)
|
||||
.skip_while(|part| part.is_empty());
|
||||
let args = parts.by_ref().take(count).map(lossy).collect();
|
||||
let env = parts
|
||||
.take_while(|part| !part.is_empty())
|
||||
.filter_map(variable)
|
||||
.collect();
|
||||
Ok((args, env))
|
||||
}
|
||||
|
||||
fn lossy(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).into_owned()
|
||||
}
|
||||
|
||||
/// A `NAME=value` variable; its value keeps its bytes.
|
||||
fn variable(pair: &[u8]) -> Option<(String, OsString)> {
|
||||
let at = pair.iter().position(|byte| *byte == b'=')?;
|
||||
Some((
|
||||
lossy(&pair[..at]),
|
||||
OsString::from_vec(pair[at + 1..].to_vec()),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+136
-5
@@ -1,7 +1,8 @@
|
||||
use std::{fs, os::unix::net::UnixListener};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A root that makes a path absolute here.
|
||||
const ROOT: &str = if cfg!(windows) { "C:/" } else { "/" };
|
||||
|
||||
fn args(words: &str) -> Vec<String> {
|
||||
words.split(' ').map(str::to_owned).collect()
|
||||
}
|
||||
@@ -73,11 +74,15 @@ fn reply(focused_pane: &str, cwd: &str) -> String {
|
||||
#[test]
|
||||
fn the_focused_pane_s_folder_comes_from_herdr_s_snapshot() {
|
||||
let read = |focused_pane: &str, cwd: &str| focused(&reply(focused_pane, cwd));
|
||||
let folder = format!("{ROOT}srv/b");
|
||||
assert_eq!(
|
||||
read(r#""w1:p2""#, r#""/srv/b""#).expect("a folder"),
|
||||
Some("/srv/b".into())
|
||||
read(r#""w1:p2""#, &format!("\"{folder}\"")).expect("a folder"),
|
||||
Some(folder.clone().into())
|
||||
);
|
||||
assert_eq!(
|
||||
read("null", &format!("\"{folder}\"")).expect("no focus"),
|
||||
None
|
||||
);
|
||||
assert_eq!(read("null", r#""/srv/b""#).expect("no focus"), None);
|
||||
assert_eq!(read(r#""w1:p2""#, "null").expect("no folder"), None);
|
||||
let error = read(r#""w1:p9""#, r#""/b""#).expect_err("a lost pane");
|
||||
assert!(error.to_string().contains("w1:p9"), "{error}");
|
||||
@@ -90,8 +95,10 @@ fn the_focused_pane_s_folder_comes_from_herdr_s_snapshot() {
|
||||
assert!(focused("").is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn herdr_answers_over_its_socket() {
|
||||
use std::{fs, os::unix::net::UnixListener};
|
||||
let dir = std::env::temp_dir().join(format!("elyterminal-herdr-{}", std::process::id()));
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(&dir).expect("a stale test folder goes");
|
||||
@@ -121,8 +128,11 @@ fn herdr_answers_over_its_socket() {
|
||||
}
|
||||
|
||||
/// macOS hides the variables of its own programs, such as `/bin/sleep`, so the test reads itself.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_process_reads_with_its_path_arguments_variables_and_folder() {
|
||||
use super::unix::{cwd, launch, path};
|
||||
use std::fs;
|
||||
let pid = std::process::id() as i32;
|
||||
let read = path(pid).expect("its path");
|
||||
let (args, env) = launch(pid).expect("its arguments");
|
||||
@@ -140,6 +150,7 @@ fn a_process_reads_with_its_path_arguments_variables_and_folder() {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn an_updated_herdr_still_names_itself() {
|
||||
use super::unix::installed;
|
||||
let deleted = PathBuf::from("/home/u/.local/bin/herdr (deleted)");
|
||||
assert_eq!(
|
||||
installed(deleted),
|
||||
@@ -154,6 +165,7 @@ fn an_updated_herdr_still_names_itself() {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_lays_out_the_arguments_after_the_program_s_path() {
|
||||
use super::unix::procargs;
|
||||
let mut buffer = 2i32.to_ne_bytes().to_vec();
|
||||
buffer.extend_from_slice(b"/usr/local/bin/herdr\0\0\0\0herdr\0\0HOME=/h\0X=a=b\0\0\0");
|
||||
let (args, env) = procargs(&buffer).expect("it parses");
|
||||
@@ -162,3 +174,122 @@ fn macos_lays_out_the_arguments_after_the_program_s_path() {
|
||||
assert_eq!(env.get("X"), Some(&OsString::from("a=b")));
|
||||
assert!(procargs(&[1, 0]).is_err());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn a_windows_client_finds_its_server_in_app_data() {
|
||||
let roaming = PathBuf::from(r"C:\Users\u\AppData\Roaming\herdr");
|
||||
let data = env(&[
|
||||
("APPDATA", r"C:\Users\u\AppData\Roaming"),
|
||||
("USERPROFILE", r"C:\D"),
|
||||
]);
|
||||
assert_eq!(found("herdr", &data), roaming.join("herdr.sock"));
|
||||
let profile = env(&[("USERPROFILE", r"C:\Users\u")]);
|
||||
assert_eq!(
|
||||
found("herdr --session work", &profile),
|
||||
roaming.join(r"sessions\work\herdr.sock")
|
||||
);
|
||||
let config = env(&[("XDG_CONFIG_HOME", r"C:\x"), ("APPDATA", r"C:\D")]);
|
||||
assert_eq!(
|
||||
found("herdr", &config),
|
||||
PathBuf::from(r"C:\x\herdr\herdr.sock")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn herdr_is_sought_nearest_under_the_shell() {
|
||||
use super::windows::nearest;
|
||||
let processes = [
|
||||
(100, 50, "pwsh.exe"),
|
||||
(104, 100, "cmd.exe"),
|
||||
(108, 104, "HERDR.EXE"),
|
||||
(112, 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);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn a_windows_process_reads_with_its_name_arguments_and_variables() {
|
||||
use super::windows::launch;
|
||||
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 (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());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn herdr_answers_over_its_pipe() {
|
||||
use std::os::windows::{
|
||||
ffi::OsStrExt,
|
||||
io::{AsRawHandle, FromRawHandle, OwnedHandle},
|
||||
};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{ERROR_PIPE_CONNECTED, INVALID_HANDLE_VALUE},
|
||||
Storage::FileSystem::PIPE_ACCESS_DUPLEX,
|
||||
System::Pipes::{ConnectNamedPipe, CreateNamedPipeW, PIPE_TYPE_BYTE},
|
||||
};
|
||||
let socket = PathBuf::from(format!(
|
||||
r"C:\elyterminal-herdr-{}\herdr.sock",
|
||||
std::process::id()
|
||||
));
|
||||
let name = format!(r"\\.\pipe\{}", socket.display());
|
||||
let name: Vec<u16> = OsStr::new(&name).encode_wide().chain([0]).collect();
|
||||
// SAFETY: `name` ends with a NUL; the handle is owned below.
|
||||
let pipe = unsafe {
|
||||
CreateNamedPipeW(
|
||||
name.as_ptr(),
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_BYTE,
|
||||
1,
|
||||
4096,
|
||||
4096,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert_ne!(pipe, INVALID_HANDLE_VALUE, "{}", io::Error::last_os_error());
|
||||
// SAFETY: the handle is new and ours.
|
||||
let pipe = unsafe { OwnedHandle::from_raw_handle(pipe) };
|
||||
let answer = reply(r#""w1:p2""#, r#""C:/srv""#);
|
||||
let server = std::thread::spawn(move || {
|
||||
// SAFETY: the pipe is open.
|
||||
let joined = unsafe { ConnectNamedPipe(pipe.as_raw_handle(), std::ptr::null_mut()) };
|
||||
let error = io::Error::last_os_error();
|
||||
assert!(
|
||||
joined != 0 || error.raw_os_error() == Some(ERROR_PIPE_CONNECTED as i32),
|
||||
"{error}"
|
||||
);
|
||||
let stream = std::fs::File::from(pipe);
|
||||
let mut request = String::new();
|
||||
BufReader::new(&stream)
|
||||
.read_line(&mut request)
|
||||
.expect("a request");
|
||||
(&stream)
|
||||
.write_all(format!("{answer}\n").as_bytes())
|
||||
.expect("a reply");
|
||||
// The pipe goes only once the client has read the reply.
|
||||
stream.sync_all().expect("the client reads the reply");
|
||||
request
|
||||
});
|
||||
assert_eq!(folder(&socket).expect("an answer"), Some("C:/srv".into()));
|
||||
assert_eq!(server.join().expect("the server").as_bytes(), SNAPSHOT);
|
||||
let error = folder(&socket).expect_err("no server");
|
||||
assert!(format!("{error:#}").contains("asking"), "{error:#}");
|
||||
assert!(starting(&error), "{error:#}");
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::unix::{ffi::OsStringExt, net::UnixStream},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
|
||||
use super::{Invocation, WAIT, follows};
|
||||
use crate::term::Pty;
|
||||
|
||||
pub(super) type Pid = i32;
|
||||
|
||||
/// herdr's executable.
|
||||
pub(super) 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>)> {
|
||||
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>> {
|
||||
let (args, env) =
|
||||
launch(group).with_context(|| format!("herdr: process {group} does not read"))?;
|
||||
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)))
|
||||
}
|
||||
|
||||
/// herdr's folder without `XDG_CONFIG_HOME`.
|
||||
pub(super) fn home(env: &HashMap<String, OsString>) -> anyhow::Result<PathBuf> {
|
||||
let home = env.get("HOME").context("herdr: its client has no HOME")?;
|
||||
Ok(PathBuf::from(home).join(".config/herdr"))
|
||||
}
|
||||
|
||||
/// herdr's socket, which must answer within `WAIT`.
|
||||
pub(super) fn connect(socket: &Path) -> io::Result<UnixStream> {
|
||||
let stream = UnixStream::connect(socket)?;
|
||||
stream.set_read_timeout(Some(WAIT))?;
|
||||
stream.set_write_timeout(Some(WAIT))?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn path(pid: Pid) -> io::Result<PathBuf> {
|
||||
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::{ffi::OsStr, 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")]
|
||||
pub(super) fn launch(pid: Pid) -> io::Result<Invocation> {
|
||||
let read = |name: &str| std::fs::read(format!("/proc/{pid}/{name}"));
|
||||
let (args, env) = (read("cmdline")?, read("environ")?);
|
||||
let args = strings(&args).map(lossy).collect();
|
||||
let env = strings(&env).filter_map(variable).collect();
|
||||
Ok((args, env))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn cwd(pid: Pid) -> 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]> {
|
||||
bytes
|
||||
.strip_suffix(b"\0")
|
||||
.unwrap_or(bytes)
|
||||
.split(|byte| *byte == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn path(pid: Pid) -> io::Result<PathBuf> {
|
||||
let mut buffer = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
|
||||
// SAFETY: the buffer holds the size given.
|
||||
let length =
|
||||
unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) };
|
||||
if length <= 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
buffer.truncate(length as usize);
|
||||
Ok(PathBuf::from(OsString::from_vec(buffer)))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn launch(pid: Pid) -> io::Result<Invocation> {
|
||||
let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid];
|
||||
let mut ask = |buffer: &mut Vec<u8>| {
|
||||
let mut size = buffer.len();
|
||||
let into = if buffer.is_empty() {
|
||||
std::ptr::null_mut()
|
||||
} else {
|
||||
buffer.as_mut_ptr().cast()
|
||||
};
|
||||
// SAFETY: `into` holds `size` bytes, or is null to ask for the size.
|
||||
let done = unsafe {
|
||||
libc::sysctl(
|
||||
name.as_mut_ptr(),
|
||||
3,
|
||||
into,
|
||||
&mut size,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if done != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(size)
|
||||
};
|
||||
let mut buffer = Vec::new();
|
||||
let size = ask(&mut buffer)?;
|
||||
buffer.resize(size, 0);
|
||||
let size = ask(&mut buffer)?;
|
||||
buffer.truncate(size);
|
||||
procargs(&buffer)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn cwd(pid: Pid) -> 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> {
|
||||
let invalid =
|
||||
|what: &str| io::Error::new(io::ErrorKind::InvalidData, format!("KERN_PROCARGS2 {what}"));
|
||||
let (count, rest) = buffer
|
||||
.split_first_chunk::<4>()
|
||||
.ok_or_else(|| invalid("holds no argument count"))?;
|
||||
let count = usize::try_from(i32::from_ne_bytes(*count))
|
||||
.map_err(|_| invalid("holds a negative argument count"))?;
|
||||
let mut parts = rest
|
||||
.split(|byte| *byte == 0)
|
||||
.skip(1)
|
||||
.skip_while(|part| part.is_empty());
|
||||
let args = parts.by_ref().take(count).map(lossy).collect();
|
||||
let env = parts
|
||||
.take_while(|part| !part.is_empty())
|
||||
.filter_map(variable)
|
||||
.collect();
|
||||
Ok((args, env))
|
||||
}
|
||||
|
||||
fn lossy(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).into_owned()
|
||||
}
|
||||
|
||||
/// A `NAME=value` variable; its value keeps its bytes.
|
||||
fn variable(pair: &[u8]) -> Option<(String, OsString)> {
|
||||
let at = pair.iter().position(|byte| *byte == b'=')?;
|
||||
Some((
|
||||
lossy(&pair[..at]),
|
||||
OsString::from_vec(pair[at + 1..].to_vec()),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{File, OpenOptions},
|
||||
io,
|
||||
os::windows::{
|
||||
ffi::{OsStrExt, OsStringExt},
|
||||
fs::OpenOptionsExt,
|
||||
io::{AsRawHandle, FromRawHandle, OwnedHandle},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{ERROR_NO_MORE_FILES, ERROR_PIPE_BUSY, INVALID_HANDLE_VALUE},
|
||||
Storage::FileSystem::SECURITY_IDENTIFICATION,
|
||||
System::{
|
||||
Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
TH32CS_SNAPPROCESS,
|
||||
},
|
||||
Pipes::WaitNamedPipeW,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{Invocation, WAIT, follows};
|
||||
use crate::term::Pty;
|
||||
|
||||
pub(super) type Pid = u32;
|
||||
|
||||
/// herdr's executable.
|
||||
pub(super) 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)))
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
at += 1;
|
||||
for process in processes.iter().filter(|(_, of, _)| *of == parent) {
|
||||
// A parent id outlives its process, so a stale one may lead back up.
|
||||
if under.contains(&process.0) {
|
||||
continue;
|
||||
}
|
||||
if process.2.eq_ignore_ascii_case(name) {
|
||||
return Some(process);
|
||||
}
|
||||
under.push(process.0);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Every process: its id, its parent's, and its executable's name.
|
||||
pub(super) fn processes() -> io::Result<Vec<(Pid, Pid, String)>> {
|
||||
// SAFETY: a plain call; its handle is owned below.
|
||||
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
||||
if snapshot == INVALID_HANDLE_VALUE {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: the handle is new and ours.
|
||||
let snapshot = unsafe { OwnedHandle::from_raw_handle(snapshot) };
|
||||
let mut entry = PROCESSENTRY32W {
|
||||
dwSize: size_of::<PROCESSENTRY32W>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mut processes = Vec::new();
|
||||
// SAFETY: `entry` holds the size it names.
|
||||
let mut read = unsafe { Process32FirstW(snapshot.as_raw_handle(), &mut entry) };
|
||||
while read != 0 {
|
||||
let name = entry.szExeFile.split(|unit| *unit == 0).next();
|
||||
processes.push((
|
||||
entry.th32ProcessID,
|
||||
entry.th32ParentProcessID,
|
||||
String::from_utf16_lossy(name.unwrap_or_default()),
|
||||
));
|
||||
// SAFETY: as above.
|
||||
read = unsafe { Process32NextW(snapshot.as_raw_handle(), &mut entry) };
|
||||
}
|
||||
let error = io::Error::last_os_error();
|
||||
// The list ends with ERROR_NO_MORE_FILES.
|
||||
if error.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) {
|
||||
Ok(processes)
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 arguments and variables of process `pid`; names fold to upper case, as Windows matches them.
|
||||
pub(super) fn launch(pid: Pid) -> io::Result<Invocation> {
|
||||
let id = sysinfo::Pid::from_u32(pid);
|
||||
let mut system = System::new();
|
||||
let read = ProcessRefreshKind::nothing()
|
||||
.with_cmd(UpdateKind::Always)
|
||||
.with_environ(UpdateKind::Always);
|
||||
system.refresh_processes_specifics(ProcessesToUpdate::Some(&[id]), true, read);
|
||||
let process = system
|
||||
.process(id)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "the process is gone"))?;
|
||||
// sysinfo leaves a list empty when it does not read; every process has both.
|
||||
if process.cmd().is_empty() || process.environ().is_empty() {
|
||||
return Err(io::Error::other(
|
||||
"its command line or variables do not read",
|
||||
));
|
||||
}
|
||||
let args = process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|arg| arg.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
let env = process
|
||||
.environ()
|
||||
.iter()
|
||||
.filter_map(|pair| variable(pair))
|
||||
.collect();
|
||||
Ok((args, env))
|
||||
}
|
||||
|
||||
/// A `NAME=value` variable, its name in upper case; its value keeps its units.
|
||||
fn variable(pair: &OsStr) -> Option<(String, OsString)> {
|
||||
let pair: Vec<u16> = pair.encode_wide().collect();
|
||||
let at = pair.iter().position(|unit| *unit == u16::from(b'='))?;
|
||||
Some((
|
||||
String::from_utf16_lossy(&pair[..at]).to_ascii_uppercase(),
|
||||
OsString::from_wide(&pair[at + 1..]),
|
||||
))
|
||||
}
|
||||
|
||||
/// herdr's folder without `XDG_CONFIG_HOME`, as herdr finds it on Windows.
|
||||
pub(super) fn home(env: &HashMap<String, OsString>) -> anyhow::Result<PathBuf> {
|
||||
if let Some(data) = env.get("APPDATA") {
|
||||
return Ok(PathBuf::from(data).join("herdr"));
|
||||
}
|
||||
if let Some(profile) = env.get("USERPROFILE") {
|
||||
return Ok(PathBuf::from(profile).join(r"AppData\Roaming\herdr"));
|
||||
}
|
||||
let home = env
|
||||
.get("HOME")
|
||||
.context("herdr: its client has no APPDATA, USERPROFILE or HOME")?;
|
||||
Ok(PathBuf::from(home).join(".config/herdr"))
|
||||
}
|
||||
|
||||
/// The pipe herdr names after its socket's path; a busy one is waited for, as herdr's client waits.
|
||||
pub(super) fn connect(socket: &Path) -> io::Result<File> {
|
||||
let pipe = format!(r"\\.\pipe\{}", socket.to_string_lossy());
|
||||
// Identification only: the pipe's server may not act as this user.
|
||||
let open = || {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.security_qos_flags(SECURITY_IDENTIFICATION)
|
||||
.open(&pipe)
|
||||
};
|
||||
match open() {
|
||||
Err(error) if error.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => {
|
||||
let name: Vec<u16> = OsStr::new(&pipe).encode_wide().chain([0]).collect();
|
||||
// SAFETY: `name` ends with a NUL.
|
||||
if unsafe { WaitNamedPipeW(name.as_ptr(), WAIT.as_millis() as u32) } == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
open()
|
||||
}
|
||||
opened => opened,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
mod boxes;
|
||||
mod find;
|
||||
pub mod frame;
|
||||
#[cfg(unix)]
|
||||
mod herdr;
|
||||
pub mod ink;
|
||||
pub mod input;
|
||||
|
||||
+7
-3
@@ -66,6 +66,8 @@ pub struct TerminalView {
|
||||
pub(super) held: HashSet<String>,
|
||||
pub(super) finder: Option<Finder>,
|
||||
pub(super) size: (usize, usize),
|
||||
/// How many folders the program has reported.
|
||||
pub(super) reports: u64,
|
||||
cell: (u16, u16),
|
||||
/// Focused in an active window, as the program last heard.
|
||||
focused: bool,
|
||||
@@ -73,7 +75,6 @@ pub struct TerminalView {
|
||||
shown: bool,
|
||||
_events: Task<()>,
|
||||
_blink: Task<()>,
|
||||
#[cfg(unix)]
|
||||
_herdr: Task<()>,
|
||||
_focus: [Subscription; 3],
|
||||
_settings: Subscription,
|
||||
@@ -113,13 +114,13 @@ impl TerminalView {
|
||||
held: HashSet::new(),
|
||||
finder: None,
|
||||
size,
|
||||
reports: 0,
|
||||
cell: (8, 16),
|
||||
focused: false,
|
||||
blinking: false,
|
||||
shown: true,
|
||||
_events: Self::pump(events, cx),
|
||||
_blink: Self::blink(cx),
|
||||
#[cfg(unix)]
|
||||
_herdr: super::herdr::follow(cx),
|
||||
_settings: cx.observe_global::<SettingsStore>(|view, cx| {
|
||||
view.pty
|
||||
@@ -211,7 +212,10 @@ impl TerminalView {
|
||||
.any(|event| matches!(event, PtyEvent::Term(Event::Wakeup)));
|
||||
for event in events {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => cx.emit(TerminalEvent::Folder(folder)),
|
||||
PtyEvent::Folder(folder) => {
|
||||
self.reports += 1;
|
||||
cx.emit(TerminalEvent::Folder(folder));
|
||||
}
|
||||
PtyEvent::Term(event) => self.term_event(event, cx),
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user