-
-
Notifications
You must be signed in to change notification settings - Fork 644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cwd-pane): Keeping the cwd when opening new panes #691
Changes from 7 commits
c22e815
9dee9c3
8d187ad
6a63bb5
c2b86db
6952972
5b1f970
1653cab
5f13c95
db65a0d
6adddce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
#[cfg(target_os = "macos")] | ||
use darwin_libproc; | ||
|
||
use std::env; | ||
use std::fs; | ||
use std::os::unix::io::RawFd; | ||
use std::os::unix::process::CommandExt; | ||
use std::path::PathBuf; | ||
|
@@ -10,7 +14,7 @@ use zellij_utils::{async_std, interprocess, libc, nix, signal_hook, zellij_tile} | |
use async_std::fs::File as AsyncFile; | ||
use async_std::os::unix::io::FromRawFd; | ||
use interprocess::local_socket::LocalSocketStream; | ||
use nix::pty::{forkpty, Winsize}; | ||
use nix::pty::{forkpty, ForkptyResult, Winsize}; | ||
use nix::sys::signal::{kill, Signal}; | ||
use nix::sys::termios; | ||
use nix::sys::wait::waitpid; | ||
|
@@ -29,6 +33,7 @@ use zellij_utils::{ | |
|
||
use async_std::io::ReadExt; | ||
pub use async_trait::async_trait; | ||
use byteorder::{BigEndian, ByteOrder}; | ||
|
||
pub use nix::unistd::Pid; | ||
|
||
|
@@ -92,44 +97,101 @@ fn handle_command_exit(mut child: Child) { | |
} | ||
} | ||
|
||
fn handle_fork_pty( | ||
fork_pty_res: ForkptyResult, | ||
cmd: RunCommand, | ||
parent_fd: RawFd, | ||
child_fd: RawFd, | ||
) -> (RawFd, Pid, RawFd) { | ||
let pid_primary = fork_pty_res.master; | ||
let (pid_secondary, pid_shell) = match fork_pty_res.fork_result { | ||
ForkResult::Parent { child } => { | ||
//fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("could not fcntl"); | ||
let pid_shell: u32 = read_from_pipe(parent_fd, child_fd); | ||
(child, pid_shell) | ||
} | ||
ForkResult::Child => { | ||
let child = unsafe { | ||
let command = &mut Command::new(cmd.command); | ||
if let Some(current_dir) = cmd.cwd { | ||
command.current_dir(current_dir); | ||
} | ||
command | ||
.args(&cmd.args) | ||
.pre_exec(|| -> std::io::Result<()> { | ||
// this is the "unsafe" part, for more details please see: | ||
// https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety | ||
unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)) | ||
.expect("failed to create a new process group"); | ||
Ok(()) | ||
}) | ||
.spawn() | ||
.expect("failed to spawn") | ||
}; | ||
unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32)) | ||
.expect("faled to set child's forceground process group"); | ||
write_to_pipe(child.id() as u32, parent_fd, child_fd); | ||
handle_command_exit(child); | ||
::std::process::exit(0); | ||
} | ||
}; | ||
(pid_primary, pid_secondary, pid_shell as i32) | ||
} | ||
|
||
/// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios) | ||
/// `orig_termios`. | ||
/// | ||
fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, Pid) { | ||
let (pid_primary, pid_secondary): (RawFd, Pid) = { | ||
fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, Pid, RawFd) { | ||
// Create a pipe to allow the child the communicate the shell's pid to it's | ||
// parent. | ||
let (parent_fd, child_fd) = unistd::pipe().expect("failed to create pipe"); | ||
let (pid_primary, pid_secondary, pid_shell): (RawFd, Pid, RawFd) = { | ||
match forkpty(None, Some(&orig_termios)) { | ||
Ok(fork_pty_res) => { | ||
let pid_primary = fork_pty_res.master; | ||
let pid_secondary = match fork_pty_res.fork_result { | ||
ForkResult::Parent { child } => child, | ||
ForkResult::Child => { | ||
let child = unsafe { | ||
Command::new(cmd.command) | ||
.args(&cmd.args) | ||
.pre_exec(|| -> std::io::Result<()> { | ||
// this is the "unsafe" part, for more details please see: | ||
// https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety | ||
unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)) | ||
.expect("failed to create a new process group"); | ||
Ok(()) | ||
}) | ||
.spawn() | ||
.expect("failed to spawn") | ||
}; | ||
unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32)) | ||
.expect("faled to set child's forceground process group"); | ||
handle_command_exit(child); | ||
::std::process::exit(0); | ||
} | ||
}; | ||
(pid_primary, pid_secondary) | ||
} | ||
Ok(fork_pty_res) => handle_fork_pty(fork_pty_res, cmd, parent_fd, child_fd), | ||
Err(e) => { | ||
panic!("failed to fork {:?}", e); | ||
} | ||
} | ||
}; | ||
(pid_primary, pid_secondary) | ||
(pid_primary, pid_secondary, pid_shell) | ||
} | ||
|
||
/// Write to a pipe given both file descriptors | ||
/// | ||
/// # Panics | ||
/// | ||
/// This function will panic if a close operation on one of the file descriptors fails or if the | ||
/// write operation fails. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little iffy about this tbh. This seems like the sort of error we should be able to recover from. If for some reason anything in this process doesn't work, we could open a pane without the CWD like we did before - no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I refactored it to never fail and to just fallback to the previous behavior. |
||
fn write_to_pipe(data: u32, parent_fd: RawFd, child_fd: RawFd) { | ||
let mut buff = [0; 4]; | ||
BigEndian::write_u32(&mut buff, data); | ||
unistd::close(parent_fd).expect("Write: couldn't close parent_fd"); | ||
match unistd::write(child_fd, &buff) { | ||
Ok(_) => {} | ||
Err(e) => { | ||
panic!("Write operation failed: {:?}", e); | ||
} | ||
} | ||
unistd::close(child_fd).expect("Write: couldn't close child_fd"); | ||
} | ||
|
||
/// Read from a pipe given both file descriptors | ||
/// | ||
/// # Panics | ||
/// | ||
/// This function will panic if a close operation on one of the file descriptors fails or if the | ||
/// read operation fails. | ||
fn read_from_pipe(parent_fd: RawFd, child_fd: RawFd) -> u32 { | ||
let mut buffer = [0; 4]; | ||
unistd::close(child_fd).expect("Read: couldn't close child_fd"); | ||
match unistd::read(parent_fd, &mut buffer) { | ||
Ok(_) => {} | ||
Err(e) => { | ||
panic!("Read operation failed: {:?}", e); | ||
} | ||
} | ||
unistd::close(parent_fd).expect("Read: couldn't close parent_fd"); | ||
u32::from_be_bytes(buffer) | ||
} | ||
|
||
/// If a [`TerminalAction::OpenFile(file)`] is given, the text editor specified by environment variable `EDITOR` | ||
|
@@ -145,11 +207,11 @@ fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, P | |
/// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not | ||
/// set. | ||
pub fn spawn_terminal( | ||
terminal_action: Option<TerminalAction>, | ||
terminal_action: TerminalAction, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the |
||
orig_termios: termios::Termios, | ||
) -> (RawFd, Pid) { | ||
) -> (RawFd, Pid, RawFd) { | ||
let cmd = match terminal_action { | ||
Some(TerminalAction::OpenFile(file_to_open)) => { | ||
TerminalAction::OpenFile(file_to_open) => { | ||
if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { | ||
panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)"); | ||
} | ||
|
@@ -160,15 +222,13 @@ pub fn spawn_terminal( | |
.into_os_string() | ||
.into_string() | ||
.expect("Not valid Utf8 Encoding")]; | ||
RunCommand { command, args } | ||
} | ||
Some(TerminalAction::RunCommand(command)) => command, | ||
None => { | ||
let command = | ||
PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")); | ||
let args = vec![]; | ||
RunCommand { command, args } | ||
RunCommand { | ||
command, | ||
args, | ||
cwd: None, | ||
} | ||
} | ||
TerminalAction::RunCommand(command) => command, | ||
}; | ||
|
||
handle_terminal(cmd, orig_termios) | ||
|
@@ -215,7 +275,7 @@ pub trait ServerOsApi: Send + Sync { | |
/// Sets the size of the terminal associated to file descriptor `fd`. | ||
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16); | ||
/// Spawn a new terminal, with a terminal action. | ||
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid); | ||
fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, Pid, RawFd); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add some comments here explaining what we're returning? It's starting to be a little confusing to me at least :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it was apparently confusing for me too given that the method was actually supposed to be returning a (RawFd, Pid, Pid). I actually refactored it to return a (RawFd, ChildId) and wrote a (hopefully) better documentation explaining what it is. Take a look and see if it's clear to you what's happening. |
||
/// Read bytes from the standard output of the virtual terminal referred to by `fd`. | ||
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; | ||
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context | ||
|
@@ -247,6 +307,8 @@ pub trait ServerOsApi: Send + Sync { | |
/// Update the receiver socket for the client | ||
fn update_receiver(&mut self, stream: LocalSocketStream); | ||
fn load_palette(&self) -> Palette; | ||
/// Returns the current working directory for a given pid | ||
fn get_cwd(&self, pid: RawFd) -> Option<PathBuf>; | ||
} | ||
|
||
impl ServerOsApi for ServerOsInputOutput { | ||
|
@@ -255,7 +317,7 @@ impl ServerOsApi for ServerOsInputOutput { | |
set_terminal_size_using_fd(fd, cols, rows); | ||
} | ||
} | ||
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid) { | ||
fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, Pid, RawFd) { | ||
let orig_termios = self.orig_termios.lock().unwrap(); | ||
spawn_terminal(terminal_action, orig_termios.clone()) | ||
} | ||
|
@@ -336,6 +398,20 @@ impl ServerOsApi for ServerOsInputOutput { | |
fn load_palette(&self) -> Palette { | ||
default_palette() | ||
} | ||
#[cfg(target_os = "macos")] | ||
fn get_cwd(&self, pid: RawFd) -> Option<PathBuf> { | ||
match darwin_libproc::pid_cwd(pid) { | ||
Ok(cwd) => Some(cwd), | ||
Err(_) => None, | ||
} | ||
} | ||
#[cfg(target_os = "linux")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have a lot of (any?) places in the code base that specifically require linux rather than unix. This might be a problem for those using Zellij on eg. FreeBSD. Do you think there can be a unix variant here? If not, can we have a unix variant that doesn't use this feature and will at least compile? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I resolved this by just returning None for non Linux/MacOS systems. I know very little about alternative Unixes so I don't really feel confident doing anything else here. I did quickly google whether or not FreeBSD has a /proc directory and apparently it does but you have to mount it first. Given that, it's probably not trivial to support everything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good solution, but correct me if I'm wrong - right now in those systems it won't compile though right? Because there is no specific function with the correct "is unix" conditional compilation? Or am I missing something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I resolved that here 5f13c95 by using: #[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] Which is admittedly a bit confusing. I couldn't just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No no, that's good - I missed it before, sorry! |
||
fn get_cwd(&self, pid: RawFd) -> Option<PathBuf> { | ||
match fs::read_link(format!("/proc/{}/cwd", pid)) { | ||
Ok(cwd) => Some(cwd), | ||
Err(_) => None, | ||
} | ||
} | ||
} | ||
|
||
impl Clone for Box<dyn ServerOsApi> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are we using
ByteOrder
for?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Unix pipe API we use to send the terminal process id back to our parent process needs a
&[u8]
. So we have to encode the u32 process id returned by the std::process::Child into a&[u8; 4]
array to send it. The byteorder crate does that for us,ByteOrder
is just the trait that implements the method to do so.