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 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 #[allow(clippy::useless_conversion)]
72 unsafe {
73 ioctl(fd, TIOCSWINSZ.into(), &winsize)
74 };
75}
76
77fn 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 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 match child.try_wait() {
96 Ok(Some(status)) => {
97 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 attempts -= 1;
117 kill(Pid::from_raw(child.id() as i32), Some(Signal::SIGTERM))
118 .with_context(err_context)?;
119 continue;
120 } else {
121 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>, 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 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
220fn 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 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
249fn 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
270fn spawn_terminal(
283 terminal_action: TerminalAction,
284 orig_termios: Option<termios::Termios>,
285 quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, default_editor: Option<PathBuf>,
287 terminal_id: u32,
288) -> Result<(RawFd, RawFd)> {
289 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 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#[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 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>>>>, cached_resizes: Arc<Mutex<Option<BTreeMap<u32, (u16, u16, Option<u16>, Option<u16>)>>>>, }
433
434#[async_trait]
437pub trait AsyncReader: Send + Sync {
438 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
439}
440
441struct RawFdAsyncReader {
443 fd: async_std::fs::File,
444}
445
446impl RawFdAsyncReader {
447 fn new(fd: RawFd) -> RawFdAsyncReader {
448 RawFdAsyncReader {
449 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
462pub 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 fn spawn_terminal(
477 &self,
478 terminal_action: TerminalAction,
479 quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, default_editor: Option<PathBuf>,
481 ) -> Result<(u32, RawFd, RawFd)>;
482 fn reserve_terminal_id(&self) -> Result<u32> {
484 unimplemented!()
485 }
486 fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize>;
488 fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader>;
490 fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize>;
492 fn tcdrain(&self, terminal_id: u32) -> Result<()>;
494 fn kill(&self, pid: Pid) -> Result<()>;
496 fn force_kill(&self, pid: Pid) -> Result<()>;
498 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 fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
510 fn get_cwds(&self, _pids: Vec<Pid>) -> (HashMap<Pid, PathBuf>, HashMap<Pid, Vec<String>>) {
512 (HashMap::new(), HashMap::new())
513 }
514 fn get_all_cmds_by_ppid(&self, _post_hook: &Option<String>) -> HashMap<String, Vec<String>> {
516 HashMap::new()
517 }
518 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>, ) -> 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>, 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 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 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 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>, ) -> Result<(RawFd, RawFd)> {
849 let default_editor = None; 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#[derive(Debug)]
952pub struct ChildId {
953 pub primary: Pid,
955 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;