Skip to content
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

Merged
merged 11 commits into from
Sep 10, 2021
57 changes: 38 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions zellij-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ license = "MIT"
ansi_term = "0.12.1"
async-trait = "0.1.50"
base64 = "0.13.0"
byteorder = "1.4.3"
daemonize = "0.4.1"
serde_json = "1.0"
unicode-width = "0.1.8"
Expand All @@ -23,6 +24,9 @@ log = "0.4.14"
typetag = "0.1.7"
chrono = "0.4.19"

[target.'cfg(target_os = "macos")'.dependencies]
darwin-libproc = "0.2.0"

[dev-dependencies]
insta = "1.6.0"

162 changes: 119 additions & 43 deletions zellij-server/src/os_input_output.rs
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;
Expand All @@ -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;
Expand All @@ -29,6 +33,7 @@ use zellij_utils::{

use async_std::io::ReadExt;
pub use async_trait::async_trait;
use byteorder::{BigEndian, ByteOrder};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we usingByteOrder for?

Copy link
Contributor Author

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.


pub use nix::unistd::Pid;

Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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`
Expand All @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the Option here and using the TerminalAction to move the cwd around is a really elegant solution.

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)");
}
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The 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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
}
Expand Down Expand Up @@ -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")]
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 #[cfg(target_family = "unix")] because Linux is considered a Unix by Rust, and it'll throw a duplicate definition error. The boolean logic there matches everything that's not either Linux or MacOS, which includes the larger family of Unix OS's Rust supports as well as Windows, Android, ..etc.

Copy link
Member

Choose a reason for hiding this comment

The 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> {
Expand Down
Loading