zellij_server/
os_input_output.rs

1use crate::{panes::PaneId, ClientId};
2
3use async_std::{fs::File as AsyncFile, io::ReadExt, os::unix::io::FromRawFd};
4use interprocess::local_socket::LocalSocketStream;
5use nix::{
6    pty::{openpty, OpenptyResult, Winsize},
7    sys::{
8        signal::{kill, Signal},
9        termios,
10    },
11    unistd,
12};
13
14use async_std;
15use interprocess;
16use libc;
17use nix;
18use signal_hook;
19use signal_hook::consts::*;
20use sysinfo::{ProcessExt, ProcessRefreshKind, System, SystemExt};
21use tempfile::tempfile;
22use zellij_utils::{
23    channels,
24    channels::TrySendError,
25    data::Palette,
26    errors::prelude::*,
27    input::command::{RunCommand, TerminalAction},
28    ipc::{
29        ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext,
30        ServerToClientMsg,
31    },
32    shared::default_palette,
33};
34
35use std::{
36    collections::{BTreeMap, BTreeSet, HashMap},
37    env,
38    fs::File,
39    io::Write,
40    os::unix::{io::RawFd, process::CommandExt},
41    path::PathBuf,
42    process::{Child, Command},
43    sync::{Arc, Mutex},
44};
45
46pub use async_trait::async_trait;
47pub use nix::unistd::Pid;
48
49fn set_terminal_size_using_fd(
50    fd: RawFd,
51    columns: u16,
52    rows: u16,
53    width_in_pixels: Option<u16>,
54    height_in_pixels: Option<u16>,
55) {
56    // TODO: do this with the nix ioctl
57    use libc::ioctl;
58    use libc::TIOCSWINSZ;
59
60    let ws_xpixel = width_in_pixels.unwrap_or(0);
61    let ws_ypixel = height_in_pixels.unwrap_or(0);
62    let winsize = Winsize {
63        ws_col: columns,
64        ws_row: rows,
65        ws_xpixel,
66        ws_ypixel,
67    };
68    // TIOCGWINSZ is an u32, but the second argument to ioctl is u64 on
69    // some platforms. When checked on Linux, clippy will complain about
70    // useless conversion.
71    #[allow(clippy::useless_conversion)]
72    unsafe {
73        ioctl(fd, TIOCSWINSZ.into(), &winsize)
74    };
75}
76
77/// Handle some signals for the child process. This will loop until the child
78/// process exits.
79fn handle_command_exit(mut child: Child) -> Result<Option<i32>> {
80    let id = child.id();
81    let err_context = || {
82        format!(
83            "failed to handle signals and command exit for child process pid {}",
84            id
85        )
86    };
87
88    // returns the exit status, if any
89    let mut should_exit = false;
90    let mut attempts = 3;
91    let mut signals =
92        signal_hook::iterator::Signals::new(&[SIGINT, SIGTERM]).with_context(err_context)?;
93    'handle_exit: loop {
94        // test whether the child process has exited
95        match child.try_wait() {
96            Ok(Some(status)) => {
97                // if the child process has exited, break outside of the loop
98                // and exit this function
99                // TODO: handle errors?
100                break 'handle_exit Ok(status.code());
101            },
102            Ok(None) => {
103                ::std::thread::sleep(::std::time::Duration::from_millis(10));
104            },
105            Err(e) => panic!("error attempting to wait: {}", e),
106        }
107
108        if !should_exit {
109            for signal in signals.pending() {
110                if signal == SIGINT || signal == SIGTERM {
111                    should_exit = true;
112                }
113            }
114        } else if attempts > 0 {
115            // let's try nicely first...
116            attempts -= 1;
117            kill(Pid::from_raw(child.id() as i32), Some(Signal::SIGTERM))
118                .with_context(err_context)?;
119            continue;
120        } else {
121            // when I say whoa, I mean WHOA!
122            let _ = child.kill();
123            break 'handle_exit Ok(None);
124        }
125    }
126}
127
128fn command_exists(cmd: &RunCommand) -> bool {
129    let command = &cmd.command;
130    match cmd.cwd.as_ref() {
131        Some(cwd) => {
132            let full_command = cwd.join(&command);
133            if full_command.exists() && full_command.is_file() {
134                return true;
135            }
136        },
137        None => {
138            if command.exists() && command.is_file() {
139                return true;
140            }
141        },
142    }
143
144    if let Some(paths) = env::var_os("PATH") {
145        for path in env::split_paths(&paths) {
146            let full_command = path.join(command);
147            if full_command.exists() && full_command.is_file() {
148                return true;
149            }
150        }
151    }
152    false
153}
154
155fn handle_openpty(
156    open_pty_res: OpenptyResult,
157    cmd: RunCommand,
158    quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
159    terminal_id: u32,
160) -> Result<(RawFd, RawFd)> {
161    let err_context = |cmd: &RunCommand| {
162        format!(
163            "failed to open PTY for command '{}'",
164            cmd.command.to_string_lossy().to_string()
165        )
166    };
167
168    // primary side of pty and child fd
169    let pid_primary = open_pty_res.master;
170    let pid_secondary = open_pty_res.slave;
171
172    if command_exists(&cmd) {
173        let mut child = unsafe {
174            let cmd = cmd.clone();
175            let command = &mut Command::new(cmd.command);
176            if let Some(current_dir) = cmd.cwd {
177                if current_dir.exists() && current_dir.is_dir() {
178                    command.current_dir(current_dir);
179                } else {
180                    log::error!(
181                        "Failed to set CWD for new pane. '{}' does not exist or is not a folder",
182                        current_dir.display()
183                    );
184                }
185            }
186            command
187                .args(&cmd.args)
188                .env("ZELLIJ_PANE_ID", &format!("{}", terminal_id))
189                .pre_exec(move || -> std::io::Result<()> {
190                    if libc::login_tty(pid_secondary) != 0 {
191                        panic!("failed to set controlling terminal");
192                    }
193                    close_fds::close_open_fds(3, &[]);
194                    Ok(())
195                })
196                .spawn()
197                .expect("failed to spawn")
198        };
199
200        let child_id = child.id();
201        std::thread::spawn(move || {
202            child.wait().with_context(|| err_context(&cmd)).fatal();
203            let exit_status = handle_command_exit(child)
204                .with_context(|| err_context(&cmd))
205                .fatal();
206            let _ = nix::unistd::close(pid_secondary);
207            quit_cb(PaneId::Terminal(terminal_id), exit_status, cmd);
208        });
209
210        Ok((pid_primary, child_id as RawFd))
211    } else {
212        Err(ZellijError::CommandNotFound {
213            terminal_id,
214            command: cmd.command.to_string_lossy().to_string(),
215        })
216        .with_context(|| err_context(&cmd))
217    }
218}
219
220/// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios)
221/// `orig_termios`.
222///
223fn handle_terminal(
224    cmd: RunCommand,
225    failover_cmd: Option<RunCommand>,
226    orig_termios: Option<termios::Termios>,
227    quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
228    terminal_id: u32,
229) -> Result<(RawFd, RawFd)> {
230    let err_context = || "failed to spawn child terminal".to_string();
231
232    // Create a pipe to allow the child the communicate the shell's pid to its
233    // parent.
234    match openpty(None, &orig_termios) {
235        Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb, terminal_id),
236        Err(e) => match failover_cmd {
237            Some(failover_cmd) => {
238                handle_terminal(failover_cmd, None, orig_termios, quit_cb, terminal_id)
239                    .with_context(err_context)
240            },
241            None => Err::<(i32, i32), _>(e)
242                .context("failed to start pty")
243                .with_context(err_context)
244                .to_log(),
245        },
246    }
247}
248
249// this is a utility method to separate the arguments from a pathbuf before we turn it into a
250// Command. eg. "/usr/bin/vim -e" ==> "/usr/bin/vim" + "-e" (the latter will be pushed to args)
251fn separate_command_arguments(command: &mut PathBuf, args: &mut Vec<String>) {
252    let mut parts = vec![];
253    let mut current_part = String::new();
254    for part in command.display().to_string().split_ascii_whitespace() {
255        current_part.push_str(part);
256        if current_part.ends_with('\\') {
257            let _ = current_part.pop();
258            current_part.push(' ');
259        } else {
260            let current_part = std::mem::replace(&mut current_part, String::new());
261            parts.push(current_part);
262        }
263    }
264    if !parts.is_empty() {
265        *command = PathBuf::from(parts.remove(0));
266        args.append(&mut parts);
267    }
268}
269
270/// If a [`TerminalAction::OpenFile(file)`] is given, the text editor specified by environment variable `EDITOR`
271/// (or `VISUAL`, if `EDITOR` is not set) will be started in the new terminal, with the given
272/// file open.
273/// If [`TerminalAction::RunCommand(RunCommand)`] is given, the command will be started
274/// in the new terminal.
275/// If None is given, the shell specified by environment variable `SHELL` will
276/// be started in the new terminal.
277///
278/// # Panics
279///
280/// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not
281/// set.
282fn spawn_terminal(
283    terminal_action: TerminalAction,
284    orig_termios: Option<termios::Termios>,
285    quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit_status
286    default_editor: Option<PathBuf>,
287    terminal_id: u32,
288) -> Result<(RawFd, RawFd)> {
289    // returns the terminal_id, the primary fd and the
290    // secondary fd
291    let mut failover_cmd_args = None;
292    let cmd = match terminal_action {
293        TerminalAction::OpenFile(mut payload) => {
294            if payload.path.is_relative() {
295                if let Some(cwd) = payload.cwd.as_ref() {
296                    payload.path = cwd.join(payload.path);
297                }
298            }
299            let mut command = default_editor.unwrap_or_else(|| {
300                PathBuf::from(
301                    env::var("EDITOR")
302                        .unwrap_or_else(|_| env::var("VISUAL").unwrap_or_else(|_| "vi".into())),
303                )
304            });
305
306            let mut args = vec![];
307
308            if !command.is_dir() {
309                separate_command_arguments(&mut command, &mut args);
310            }
311            let file_to_open = payload
312                .path
313                .into_os_string()
314                .into_string()
315                .expect("Not valid Utf8 Encoding");
316            if let Some(line_number) = payload.line_number {
317                if command.ends_with("vim")
318                    || command.ends_with("nvim")
319                    || command.ends_with("emacs")
320                    || command.ends_with("nano")
321                    || command.ends_with("kak")
322                {
323                    failover_cmd_args = Some(vec![file_to_open.clone()]);
324                    args.push(format!("+{}", line_number));
325                    args.push(file_to_open);
326                } else if command.ends_with("hx") || command.ends_with("helix") {
327                    // at the time of writing, helix only supports this syntax
328                    // and it might be a good idea to leave this here anyway
329                    // to keep supporting old versions
330                    args.push(format!("{}:{}", file_to_open, line_number));
331                } else {
332                    args.push(file_to_open);
333                }
334            } else {
335                args.push(file_to_open);
336            }
337            RunCommand {
338                command,
339                args,
340                cwd: payload.cwd,
341                hold_on_close: false,
342                hold_on_start: false,
343                ..Default::default()
344            }
345        },
346        TerminalAction::RunCommand(command) => command,
347    };
348    let failover_cmd = if let Some(failover_cmd_args) = failover_cmd_args {
349        let mut cmd = cmd.clone();
350        cmd.args = failover_cmd_args;
351        Some(cmd)
352    } else {
353        None
354    };
355
356    handle_terminal(cmd, failover_cmd, orig_termios, quit_cb, terminal_id)
357}
358
359// The ClientSender is in charge of sending messages to the client on a special thread
360// This is done so that when the unix socket buffer is full, we won't block the entire router
361// thread
362// When the above happens, the ClientSender buffers messages in hopes that the congestion will be
363// freed until we runs out of buffer space.
364// If we run out of buffer space, we bubble up an error sot hat the router thread will give up on
365// this client and we'll stop sending messages to it.
366// If the client ever becomes responsive again, we'll send one final "Buffer full" message so it
367// knows what happened.
368#[derive(Clone)]
369struct ClientSender {
370    client_id: ClientId,
371    client_buffer_sender: channels::Sender<ServerToClientMsg>,
372}
373
374impl ClientSender {
375    pub fn new(client_id: ClientId, mut sender: IpcSenderWithContext<ServerToClientMsg>) -> Self {
376        // FIXME(hartan): This queue is responsible for buffering messages between server and
377        // client. If it fills up, the client is disconnected with a "Buffer full" sort of error
378        // message. It was previously found to be too small (with depth 50), so it was increased to
379        // 5000 instead. This decision was made because it was found that a queue of depth 5000
380        // doesn't cause noticable increase in RAM usage, but there's no reason beyond that. If in
381        // the future this is found to fill up too quickly again, it may be worthwhile to increase
382        // the size even further (or better yet, implement a redraw-on-backpressure mechanism).
383        // We, the zellij maintainers, have decided against an unbounded
384        // queue for the time being because we want to prevent e.g. the whole session being killed
385        // (by OOM-killers or some other mechanism) just because a single client doesn't respond.
386        let (client_buffer_sender, client_buffer_receiver) = channels::bounded(5000);
387        std::thread::spawn(move || {
388            let err_context = || format!("failed to send message to client {client_id}");
389            for msg in client_buffer_receiver.iter() {
390                sender.send(msg).with_context(err_context).non_fatal();
391            }
392            let _ = sender.send(ServerToClientMsg::Exit(ExitReason::Disconnect));
393        });
394        ClientSender {
395            client_id,
396            client_buffer_sender,
397        }
398    }
399    pub fn send_or_buffer(&self, msg: ServerToClientMsg) -> Result<()> {
400        let err_context = || {
401            format!(
402                "failed to send or buffer message for client {}",
403                self.client_id
404            )
405        };
406
407        self.client_buffer_sender
408            .try_send(msg)
409            .or_else(|err| {
410                if let TrySendError::Full(_) = err {
411                    log::warn!(
412                        "client {} is processing server messages too slow",
413                        self.client_id
414                    );
415                }
416                Err(err)
417            })
418            .with_context(err_context)
419    }
420}
421
422#[derive(Clone)]
423pub struct ServerOsInputOutput {
424    orig_termios: Arc<Mutex<Option<termios::Termios>>>,
425    client_senders: Arc<Mutex<HashMap<ClientId, ClientSender>>>,
426    terminal_id_to_raw_fd: Arc<Mutex<BTreeMap<u32, Option<RawFd>>>>, // A value of None means the
427    // terminal_id exists but is
428    // not connected to an fd (eg.
429    // a command pane with a
430    // non-existing command)
431    cached_resizes: Arc<Mutex<Option<BTreeMap<u32, (u16, u16, Option<u16>, Option<u16>)>>>>, // <terminal_id, (cols, rows, width_in_pixels, height_in_pixels)>
432}
433
434// async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being
435// used. See https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/
436#[async_trait]
437pub trait AsyncReader: Send + Sync {
438    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
439}
440
441/// An `AsyncReader` that wraps a `RawFd`
442struct RawFdAsyncReader {
443    fd: async_std::fs::File,
444}
445
446impl RawFdAsyncReader {
447    fn new(fd: RawFd) -> RawFdAsyncReader {
448        RawFdAsyncReader {
449            // The supplied `RawFd` is consumed by the created `RawFdAsyncReader`, closing it when dropped
450            fd: unsafe { AsyncFile::from_raw_fd(fd) },
451        }
452    }
453}
454
455#[async_trait]
456impl AsyncReader for RawFdAsyncReader {
457    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
458        self.fd.read(buf).await
459    }
460}
461
462/// The `ServerOsApi` trait represents an abstract interface to the features of an operating system that
463/// Zellij server requires.
464pub trait ServerOsApi: Send + Sync {
465    fn set_terminal_size_using_terminal_id(
466        &self,
467        id: u32,
468        cols: u16,
469        rows: u16,
470        width_in_pixels: Option<u16>,
471        height_in_pixels: Option<u16>,
472    ) -> Result<()>;
473    /// Spawn a new terminal, with a terminal action. The returned tuple contains the master file
474    /// descriptor of the forked pseudo terminal and a [ChildId] struct containing process id's for
475    /// the forked child process.
476    fn spawn_terminal(
477        &self,
478        terminal_action: TerminalAction,
479        quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
480        default_editor: Option<PathBuf>,
481    ) -> Result<(u32, RawFd, RawFd)>;
482    // reserves a terminal id without actually opening a terminal
483    fn reserve_terminal_id(&self) -> Result<u32> {
484        unimplemented!()
485    }
486    /// Read bytes from the standard output of the virtual terminal referred to by `fd`.
487    fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize>;
488    /// Creates an `AsyncReader` that can be used to read from `fd` in an async context
489    fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader>;
490    /// Write bytes to the standard input of the virtual terminal referred to by `fd`.
491    fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize>;
492    /// Wait until all output written to the object referred to by `fd` has been transmitted.
493    fn tcdrain(&self, terminal_id: u32) -> Result<()>;
494    /// Terminate the process with process ID `pid`. (SIGTERM)
495    fn kill(&self, pid: Pid) -> Result<()>;
496    /// Terminate the process with process ID `pid`. (SIGKILL)
497    fn force_kill(&self, pid: Pid) -> Result<()>;
498    /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct.
499    fn box_clone(&self) -> Box<dyn ServerOsApi>;
500    fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()>;
501    fn new_client(
502        &mut self,
503        client_id: ClientId,
504        stream: LocalSocketStream,
505    ) -> Result<IpcReceiverWithContext<ClientToServerMsg>>;
506    fn remove_client(&mut self, client_id: ClientId) -> Result<()>;
507    fn load_palette(&self) -> Palette;
508    /// Returns the current working directory for a given pid
509    fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
510    /// Returns the current working directory for multiple pids
511    fn get_cwds(&self, _pids: Vec<Pid>) -> (HashMap<Pid, PathBuf>, HashMap<Pid, Vec<String>>) {
512        (HashMap::new(), HashMap::new())
513    }
514    /// Get a list of all running commands by their parent process id
515    fn get_all_cmds_by_ppid(&self, _post_hook: &Option<String>) -> HashMap<String, Vec<String>> {
516        HashMap::new()
517    }
518    /// Writes the given buffer to a string
519    fn write_to_file(&mut self, buf: String, file: Option<String>) -> Result<()>;
520
521    fn re_run_command_in_terminal(
522        &self,
523        terminal_id: u32,
524        run_command: RunCommand,
525        quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
526    ) -> Result<(RawFd, RawFd)>;
527    fn clear_terminal_id(&self, terminal_id: u32) -> Result<()>;
528    fn cache_resizes(&mut self) {}
529    fn apply_cached_resizes(&mut self) {}
530}
531
532impl ServerOsApi for ServerOsInputOutput {
533    fn set_terminal_size_using_terminal_id(
534        &self,
535        id: u32,
536        cols: u16,
537        rows: u16,
538        width_in_pixels: Option<u16>,
539        height_in_pixels: Option<u16>,
540    ) -> Result<()> {
541        let err_context = || {
542            format!(
543                "failed to set terminal id {} to size ({}, {})",
544                id, rows, cols
545            )
546        };
547        if let Some(cached_resizes) = self.cached_resizes.lock().unwrap().as_mut() {
548            cached_resizes.insert(id, (cols, rows, width_in_pixels, height_in_pixels));
549            return Ok(());
550        }
551
552        match self
553            .terminal_id_to_raw_fd
554            .lock()
555            .to_anyhow()
556            .with_context(err_context)?
557            .get(&id)
558        {
559            Some(Some(fd)) => {
560                if cols > 0 && rows > 0 {
561                    set_terminal_size_using_fd(*fd, cols, rows, width_in_pixels, height_in_pixels);
562                }
563            },
564            _ => {
565                Err::<(), _>(anyhow!("failed to find terminal fd for id {id}"))
566                    .with_context(err_context)
567                    .non_fatal();
568            },
569        }
570        Ok(())
571    }
572    #[allow(unused_assignments)]
573    fn spawn_terminal(
574        &self,
575        terminal_action: TerminalAction,
576        quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
577        default_editor: Option<PathBuf>,
578    ) -> Result<(u32, RawFd, RawFd)> {
579        let err_context = || "failed to spawn terminal".to_string();
580
581        let orig_termios = self
582            .orig_termios
583            .lock()
584            .to_anyhow()
585            .with_context(err_context)?;
586        let terminal_id = self
587            .terminal_id_to_raw_fd
588            .lock()
589            .to_anyhow()
590            .with_context(err_context)?
591            .keys()
592            .copied()
593            .collect::<BTreeSet<u32>>()
594            .last()
595            .map(|l| l + 1)
596            .or(Some(0));
597        match terminal_id {
598            Some(terminal_id) => {
599                self.terminal_id_to_raw_fd
600                    .lock()
601                    .to_anyhow()
602                    .with_context(err_context)?
603                    .insert(terminal_id, None);
604                spawn_terminal(
605                    terminal_action,
606                    orig_termios.clone(),
607                    quit_cb,
608                    default_editor,
609                    terminal_id,
610                )
611                .and_then(|(pid_primary, pid_secondary)| {
612                    self.terminal_id_to_raw_fd
613                        .lock()
614                        .to_anyhow()?
615                        .insert(terminal_id, Some(pid_primary));
616                    Ok((terminal_id, pid_primary, pid_secondary))
617                })
618                .with_context(err_context)
619            },
620            None => Err(anyhow!("no more terminal IDs left to allocate")),
621        }
622    }
623    #[allow(unused_assignments)]
624    fn reserve_terminal_id(&self) -> Result<u32> {
625        let err_context = || "failed to reserve a terminal ID".to_string();
626
627        let terminal_id = self
628            .terminal_id_to_raw_fd
629            .lock()
630            .to_anyhow()
631            .with_context(err_context)?
632            .keys()
633            .copied()
634            .collect::<BTreeSet<u32>>()
635            .last()
636            .map(|l| l + 1)
637            .or(Some(0));
638        match terminal_id {
639            Some(terminal_id) => {
640                self.terminal_id_to_raw_fd
641                    .lock()
642                    .to_anyhow()
643                    .with_context(err_context)?
644                    .insert(terminal_id, None);
645                Ok(terminal_id)
646            },
647            None => Err(anyhow!("no more terminal IDs available")),
648        }
649    }
650    fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize> {
651        unistd::read(fd, buf).with_context(|| format!("failed to read stdout of raw FD {}", fd))
652    }
653    fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
654        Box::new(RawFdAsyncReader::new(fd))
655    }
656    fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize> {
657        let err_context = || format!("failed to write to stdin of TTY ID {}", terminal_id);
658
659        match self
660            .terminal_id_to_raw_fd
661            .lock()
662            .to_anyhow()
663            .with_context(err_context)?
664            .get(&terminal_id)
665        {
666            Some(Some(fd)) => unistd::write(*fd, buf).with_context(err_context),
667            _ => Err(anyhow!("could not find raw file descriptor")).with_context(err_context),
668        }
669    }
670    fn tcdrain(&self, terminal_id: u32) -> Result<()> {
671        let err_context = || format!("failed to tcdrain to TTY ID {}", terminal_id);
672
673        match self
674            .terminal_id_to_raw_fd
675            .lock()
676            .to_anyhow()
677            .with_context(err_context)?
678            .get(&terminal_id)
679        {
680            Some(Some(fd)) => termios::tcdrain(*fd).with_context(err_context),
681            _ => Err(anyhow!("could not find raw file descriptor")).with_context(err_context),
682        }
683    }
684    fn box_clone(&self) -> Box<dyn ServerOsApi> {
685        Box::new((*self).clone())
686    }
687    fn kill(&self, pid: Pid) -> Result<()> {
688        let _ = kill(pid, Some(Signal::SIGHUP));
689        Ok(())
690    }
691    fn force_kill(&self, pid: Pid) -> Result<()> {
692        let _ = kill(pid, Some(Signal::SIGKILL));
693        Ok(())
694    }
695    fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()> {
696        let err_context = || format!("failed to send message to client {client_id}");
697
698        if let Some(sender) = self
699            .client_senders
700            .lock()
701            .to_anyhow()
702            .with_context(err_context)?
703            .get_mut(&client_id)
704        {
705            sender.send_or_buffer(msg).with_context(err_context)
706        } else {
707            Ok(())
708        }
709    }
710
711    fn new_client(
712        &mut self,
713        client_id: ClientId,
714        stream: LocalSocketStream,
715    ) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
716        let receiver = IpcReceiverWithContext::new(stream);
717        let sender = ClientSender::new(client_id, receiver.get_sender());
718        self.client_senders
719            .lock()
720            .to_anyhow()
721            .with_context(|| format!("failed to create new client {client_id}"))?
722            .insert(client_id, sender);
723        Ok(receiver)
724    }
725
726    fn remove_client(&mut self, client_id: ClientId) -> Result<()> {
727        let mut client_senders = self
728            .client_senders
729            .lock()
730            .to_anyhow()
731            .with_context(|| format!("failed to remove client {client_id}"))?;
732        if client_senders.contains_key(&client_id) {
733            client_senders.remove(&client_id);
734        }
735        Ok(())
736    }
737
738    fn load_palette(&self) -> Palette {
739        default_palette()
740    }
741
742    fn get_cwd(&self, pid: Pid) -> Option<PathBuf> {
743        let mut system_info = System::new();
744        // Update by minimizing information.
745        // See https://docs.rs/sysinfo/0.22.5/sysinfo/struct.ProcessRefreshKind.html#
746        system_info.refresh_process_specifics(pid.into(), ProcessRefreshKind::default());
747
748        if let Some(process) = system_info.process(pid.into()) {
749            let cwd = process.cwd();
750            let cwd_is_empty = cwd.iter().next().is_none();
751            if !cwd_is_empty {
752                return Some(process.cwd().to_path_buf());
753            }
754        }
755        None
756    }
757
758    fn get_cwds(&self, pids: Vec<Pid>) -> (HashMap<Pid, PathBuf>, HashMap<Pid, Vec<String>>) {
759        let mut system_info = System::new();
760        let mut cwds = HashMap::new();
761        let mut cmds = HashMap::new();
762
763        for pid in pids {
764            // Update by minimizing information.
765            // See https://docs.rs/sysinfo/0.22.5/sysinfo/struct.ProcessRefreshKind.html#
766            let is_found =
767                system_info.refresh_process_specifics(pid.into(), ProcessRefreshKind::default());
768            if is_found {
769                if let Some(process) = system_info.process(pid.into()) {
770                    let cwd = process.cwd();
771                    let cmd = process.cmd();
772                    let cwd_is_empty = cwd.iter().next().is_none();
773                    if !cwd_is_empty {
774                        cwds.insert(pid, process.cwd().to_path_buf());
775                    }
776                    let cmd_is_empty = cmd.iter().next().is_none();
777                    if !cmd_is_empty {
778                        cmds.insert(pid, process.cmd().to_vec());
779                    }
780                }
781            }
782        }
783
784        (cwds, cmds)
785    }
786    fn get_all_cmds_by_ppid(&self, post_hook: &Option<String>) -> HashMap<String, Vec<String>> {
787        // the key is the stringified ppid
788        let mut cmds = HashMap::new();
789        if let Some(output) = Command::new("ps")
790            .args(vec!["-ao", "ppid,args"])
791            .output()
792            .ok()
793        {
794            let output = String::from_utf8(output.stdout.clone())
795                .unwrap_or_else(|_| String::from_utf8_lossy(&output.stdout).to_string());
796            for line in output.lines() {
797                let line_parts: Vec<String> = line
798                    .trim()
799                    .split_ascii_whitespace()
800                    .map(|p| p.to_owned())
801                    .collect();
802                let mut line_parts = line_parts.into_iter();
803                let ppid = line_parts.next();
804                if let Some(ppid) = ppid {
805                    match &post_hook {
806                        Some(post_hook) => {
807                            let command: Vec<String> = line_parts.clone().collect();
808                            let stringified = command.join(" ");
809                            let cmd = match run_command_hook(&stringified, post_hook) {
810                                Ok(command) => command,
811                                Err(e) => {
812                                    log::error!("Post command hook failed to run: {}", e);
813                                    stringified.to_owned()
814                                },
815                            };
816                            let line_parts: Vec<String> = cmd
817                                .trim()
818                                .split_ascii_whitespace()
819                                .map(|p| p.to_owned())
820                                .collect();
821                            cmds.insert(ppid.into(), line_parts);
822                        },
823                        None => {
824                            cmds.insert(ppid.into(), line_parts.collect());
825                        },
826                    }
827                }
828            }
829        }
830        cmds
831    }
832
833    fn write_to_file(&mut self, buf: String, name: Option<String>) -> Result<()> {
834        let err_context = || "failed to write to file".to_string();
835
836        let mut f: File = match name {
837            Some(x) => File::create(x).with_context(err_context)?,
838            None => tempfile().with_context(err_context)?,
839        };
840        write!(f, "{}", buf).with_context(err_context)
841    }
842
843    fn re_run_command_in_terminal(
844        &self,
845        terminal_id: u32,
846        run_command: RunCommand,
847        quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
848    ) -> Result<(RawFd, RawFd)> {
849        let default_editor = None; // no need for a default editor when running an explicit command
850        self.orig_termios
851            .lock()
852            .to_anyhow()
853            .and_then(|orig_termios| {
854                spawn_terminal(
855                    TerminalAction::RunCommand(run_command),
856                    orig_termios.clone(),
857                    quit_cb,
858                    default_editor,
859                    terminal_id,
860                )
861            })
862            .and_then(|(pid_primary, pid_secondary)| {
863                self.terminal_id_to_raw_fd
864                    .lock()
865                    .to_anyhow()?
866                    .insert(terminal_id, Some(pid_primary));
867                Ok((pid_primary, pid_secondary))
868            })
869            .with_context(|| format!("failed to rerun command in terminal id {}", terminal_id))
870    }
871    fn clear_terminal_id(&self, terminal_id: u32) -> Result<()> {
872        self.terminal_id_to_raw_fd
873            .lock()
874            .to_anyhow()
875            .with_context(|| format!("failed to clear terminal ID {}", terminal_id))?
876            .remove(&terminal_id);
877        Ok(())
878    }
879    fn cache_resizes(&mut self) {
880        if self.cached_resizes.lock().unwrap().is_none() {
881            *self.cached_resizes.lock().unwrap() = Some(BTreeMap::new());
882        }
883    }
884    fn apply_cached_resizes(&mut self) {
885        let mut cached_resizes = self.cached_resizes.lock().unwrap().take();
886        if let Some(cached_resizes) = cached_resizes.as_mut() {
887            for (terminal_id, (cols, rows, width_in_pixels, height_in_pixels)) in
888                cached_resizes.iter()
889            {
890                let _ = self.set_terminal_size_using_terminal_id(
891                    *terminal_id,
892                    *cols,
893                    *rows,
894                    width_in_pixels.clone(),
895                    height_in_pixels.clone(),
896                );
897            }
898        }
899    }
900}
901
902impl Clone for Box<dyn ServerOsApi> {
903    fn clone(&self) -> Box<dyn ServerOsApi> {
904        self.box_clone()
905    }
906}
907
908pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
909    let current_termios = termios::tcgetattr(0).ok();
910    if current_termios.is_none() {
911        log::warn!("Starting a server without a controlling terminal, using the default termios configuration.");
912    }
913    let orig_termios = Arc::new(Mutex::new(current_termios));
914    Ok(ServerOsInputOutput {
915        orig_termios,
916        client_senders: Arc::new(Mutex::new(HashMap::new())),
917        terminal_id_to_raw_fd: Arc::new(Mutex::new(BTreeMap::new())),
918        cached_resizes: Arc::new(Mutex::new(None)),
919    })
920}
921
922use crate::pty_writer::PtyWriteInstruction;
923use crate::thread_bus::ThreadSenders;
924
925pub struct ResizeCache {
926    senders: ThreadSenders,
927}
928
929impl ResizeCache {
930    pub fn new(senders: ThreadSenders) -> Self {
931        senders
932            .send_to_pty_writer(PtyWriteInstruction::StartCachingResizes)
933            .unwrap_or_else(|e| {
934                log::error!("Failed to cache resizes: {}", e);
935            });
936        ResizeCache { senders }
937    }
938}
939
940impl Drop for ResizeCache {
941    fn drop(&mut self) {
942        self.senders
943            .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
944            .unwrap_or_else(|e| {
945                log::error!("Failed to apply cached resizes: {}", e);
946            });
947    }
948}
949
950/// Process id's for forked terminals
951#[derive(Debug)]
952pub struct ChildId {
953    /// Primary process id of a forked terminal
954    pub primary: Pid,
955    /// Process id of the command running inside the forked terminal, usually a shell. The primary
956    /// field is it's parent process id.
957    pub shell: Option<Pid>,
958}
959
960fn run_command_hook(
961    original_command: &str,
962    hook_script: &str,
963) -> Result<String, Box<dyn std::error::Error>> {
964    let output = Command::new("sh")
965        .arg("-c")
966        .arg(hook_script)
967        .env("RESURRECT_COMMAND", original_command)
968        .output()?;
969
970    if !output.status.success() {
971        return Err(format!("Hook failed: {}", String::from_utf8_lossy(&output.stderr)).into());
972    }
973    Ok(String::from_utf8(output.stdout)?.trim().to_string())
974}
975
976#[cfg(test)]
977#[path = "./unit/os_input_output_tests.rs"]
978mod os_input_output_tests;