1use std::ffi::CStr;
4use std::fs::File;
5use std::io::{Error, ErrorKind, Read, Result};
6use std::mem::MaybeUninit;
7use std::os::fd::OwnedFd;
8use std::os::unix::io::AsRawFd;
9use std::os::unix::net::UnixStream;
10use std::os::unix::process::CommandExt;
11#[cfg(target_os = "macos")]
12use std::path::Path;
13use std::process::{Child, Command};
14use std::sync::Arc;
15use std::{env, ptr};
16
17use libc::{F_GETFL, F_SETFL, O_NONBLOCK, TIOCSCTTY, c_int, fcntl};
18use log::error;
19use polling::{Event, PollMode, Poller};
20use rustix_openpty::openpty;
21use rustix_openpty::rustix::termios::Winsize;
22#[cfg(any(target_os = "linux", target_os = "macos"))]
23use rustix_openpty::rustix::termios::{self, InputModes, OptionalActions};
24use signal_hook::low_level::{pipe as signal_pipe, unregister as unregister_signal};
25use signal_hook::{SigId, consts as sigconsts};
26
27use crate::event::{OnResize, WindowSize};
28use crate::tty::{ChildEvent, EventedPty, EventedReadWrite, Options};
29
30pub(crate) const PTY_READ_WRITE_TOKEN: usize = 0;
32
33pub(crate) const PTY_CHILD_EVENT_TOKEN: usize = 1;
35
36macro_rules! die {
37 ($($arg:tt)*) => {{
38 error!($($arg)*);
39 std::process::exit(1);
40 }};
41}
42
43fn set_controlling_terminal(fd: c_int) {
45 let res = unsafe {
46 #[allow(clippy::cast_lossless)]
51 libc::ioctl(fd, TIOCSCTTY as _, 0)
52 };
53
54 if res < 0 {
55 die!("ioctl TIOCSCTTY failed: {}", Error::last_os_error());
56 }
57}
58
59#[derive(Debug)]
60struct Passwd<'a> {
61 name: &'a str,
62 dir: &'a str,
63 shell: &'a str,
64}
65
66fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
72 let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
74
75 let mut res: *mut libc::passwd = ptr::null_mut();
76
77 let uid = unsafe { libc::getuid() };
79 let status = unsafe {
80 libc::getpwuid_r(uid, entry.as_mut_ptr(), buf.as_mut_ptr() as *mut _, buf.len(), &mut res)
81 };
82 let entry = unsafe { entry.assume_init() };
83
84 if status < 0 {
85 return Err(Error::other("getpwuid_r failed"));
86 }
87
88 if res.is_null() {
89 return Err(Error::other("pw not found"));
90 }
91
92 assert_eq!(entry.pw_uid, uid);
94
95 Ok(Passwd {
97 name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
98 dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
99 shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
100 })
101}
102
103pub struct Pty {
104 child: Child,
105 file: File,
106 signals: UnixStream,
107 sig_id: SigId,
108}
109
110impl Pty {
111 pub fn child(&self) -> &Child {
112 &self.child
113 }
114
115 pub fn file(&self) -> &File {
116 &self.file
117 }
118}
119
120struct ShellUser {
122 user: String,
123 home: String,
124 shell: String,
125}
126
127impl ShellUser {
128 fn from_env() -> Result<Self> {
131 let mut buf = [0; 1024];
132 let pw = get_pw_entry(&mut buf);
133
134 let user = match env::var("USER") {
135 Ok(user) => user,
136 Err(_) => match pw {
137 Ok(ref pw) => pw.name.to_owned(),
138 Err(err) => return Err(err),
139 },
140 };
141
142 let home = match env::var("HOME") {
143 Ok(home) => home,
144 Err(_) => match pw {
145 Ok(ref pw) => pw.dir.to_owned(),
146 Err(err) => return Err(err),
147 },
148 };
149
150 let shell = match env::var("SHELL") {
151 Ok(shell) => shell,
152 Err(_) => match pw {
153 Ok(ref pw) => pw.shell.to_owned(),
154 Err(err) => return Err(err),
155 },
156 };
157
158 Ok(Self { user, home, shell })
159 }
160}
161
162#[cfg(not(target_os = "macos"))]
163fn default_shell_command(shell: &str, _user: &str, _home: &str) -> Command {
164 Command::new(shell)
165}
166
167#[cfg(target_os = "macos")]
168fn default_shell_command(shell: &str, user: &str, home: &str) -> Command {
169 let shell_name = shell.rsplit('/').next().unwrap();
170
171 let mut login_command = Command::new("/usr/bin/login");
173
174 let exec = format!("exec -a -{} {}", shell_name, shell);
177
178 let has_home_hushlogin = Path::new(home).join(".hushlogin").exists();
183
184 let flags = if has_home_hushlogin { "-qflp" } else { "-flp" };
191 login_command.args([flags, user, "/bin/zsh", "-fc", &exec]);
192 login_command
193}
194
195pub fn new(config: &Options, window_size: WindowSize, window_id: u64) -> Result<Pty> {
197 let pty = openpty(None, Some(&window_size.to_winsize()))?;
198 let (master, slave) = (pty.controller, pty.user);
199 from_fd(config, window_id, master, slave)
200}
201
202pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd) -> Result<Pty> {
204 let master_fd = master.as_raw_fd();
205 let slave_fd = slave.as_raw_fd();
206
207 #[cfg(any(target_os = "linux", target_os = "macos"))]
208 if let Ok(mut termios) = termios::tcgetattr(&master) {
209 termios.input_modes.set(InputModes::IUTF8, true);
211 let _ = termios::tcsetattr(&master, OptionalActions::Now, &termios);
212 }
213
214 let user = ShellUser::from_env()?;
215
216 let mut builder = if let Some(shell) = config.shell.as_ref() {
217 let mut cmd = Command::new(&shell.program);
218 cmd.args(shell.args.as_slice());
219 cmd
220 } else {
221 default_shell_command(&user.shell, &user.user, &user.home)
222 };
223
224 builder.stdin(slave.try_clone()?);
226 builder.stderr(slave.try_clone()?);
227 builder.stdout(slave);
228
229 let window_id = window_id.to_string();
231 builder.env("ALACRITTY_WINDOW_ID", &window_id);
232 builder.env("USER", user.user);
233 builder.env("HOME", user.home);
234 builder.env("WINDOWID", window_id);
236 for (key, value) in &config.env {
237 builder.env(key, value);
238 }
239
240 builder.env_remove("XDG_ACTIVATION_TOKEN");
242 builder.env_remove("DESKTOP_STARTUP_ID");
243
244 let working_directory = config.working_directory.clone();
245 unsafe {
246 builder.pre_exec(move || {
247 let err = libc::setsid();
249 if err == -1 {
250 return Err(Error::other("Failed to set session id"));
251 }
252
253 if let Some(working_directory) = working_directory.as_ref() {
255 let _ = env::set_current_dir(working_directory);
256 }
257
258 set_controlling_terminal(slave_fd);
259
260 libc::close(slave_fd);
262 libc::close(master_fd);
263
264 libc::signal(libc::SIGCHLD, libc::SIG_DFL);
265 libc::signal(libc::SIGHUP, libc::SIG_DFL);
266 libc::signal(libc::SIGINT, libc::SIG_DFL);
267 libc::signal(libc::SIGQUIT, libc::SIG_DFL);
268 libc::signal(libc::SIGTERM, libc::SIG_DFL);
269 libc::signal(libc::SIGALRM, libc::SIG_DFL);
270
271 Ok(())
272 });
273 }
274
275 let (signals, sig_id) = {
277 let (sender, recv) = UnixStream::pair()?;
278
279 let sig_id = signal_pipe::register(sigconsts::SIGCHLD, sender)?;
281 recv.set_nonblocking(true)?;
282 (recv, sig_id)
283 };
284
285 match builder.spawn() {
286 Ok(child) => {
287 unsafe {
288 set_nonblocking(master_fd);
291 }
292
293 Ok(Pty { child, file: File::from(master), signals, sig_id })
294 },
295 Err(err) => Err(Error::new(
296 err.kind(),
297 format!(
298 "Failed to spawn command '{}': {}",
299 builder.get_program().to_string_lossy(),
300 err
301 ),
302 )),
303 }
304}
305
306impl Drop for Pty {
307 fn drop(&mut self) {
308 unsafe {
310 libc::kill(self.child.id() as i32, libc::SIGHUP);
311 }
312
313 unregister_signal(self.sig_id);
315
316 let _ = self.child.wait();
317 }
318}
319
320impl EventedReadWrite for Pty {
321 type Reader = File;
322 type Writer = File;
323
324 #[inline]
325 unsafe fn register(
326 &mut self,
327 poll: &Arc<Poller>,
328 mut interest: Event,
329 poll_opts: PollMode,
330 ) -> Result<()> {
331 interest.key = PTY_READ_WRITE_TOKEN;
332 unsafe {
333 poll.add_with_mode(&self.file, interest, poll_opts)?;
334 }
335
336 unsafe {
337 poll.add_with_mode(
338 &self.signals,
339 Event::readable(PTY_CHILD_EVENT_TOKEN),
340 PollMode::Level,
341 )
342 }
343 }
344
345 #[inline]
346 fn reregister(
347 &mut self,
348 poll: &Arc<Poller>,
349 mut interest: Event,
350 poll_opts: PollMode,
351 ) -> Result<()> {
352 interest.key = PTY_READ_WRITE_TOKEN;
353 poll.modify_with_mode(&self.file, interest, poll_opts)?;
354
355 poll.modify_with_mode(
356 &self.signals,
357 Event::readable(PTY_CHILD_EVENT_TOKEN),
358 PollMode::Level,
359 )
360 }
361
362 #[inline]
363 fn deregister(&mut self, poll: &Arc<Poller>) -> Result<()> {
364 poll.delete(&self.file)?;
365 poll.delete(&self.signals)
366 }
367
368 #[inline]
369 fn reader(&mut self) -> &mut File {
370 &mut self.file
371 }
372
373 #[inline]
374 fn writer(&mut self) -> &mut File {
375 &mut self.file
376 }
377}
378
379impl EventedPty for Pty {
380 #[inline]
381 fn next_child_event(&mut self) -> Option<ChildEvent> {
382 let mut buf = [0u8; 1];
384 if let Err(err) = self.signals.read(&mut buf) {
385 if err.kind() != ErrorKind::WouldBlock {
386 error!("Error reading from signal pipe: {err}");
387 }
388 return None;
389 }
390
391 match self.child.try_wait() {
393 Err(err) => {
394 error!("Error checking child process termination: {err}");
395 None
396 },
397 Ok(None) => None,
398 Ok(exit_status) => Some(ChildEvent::Exited(exit_status.and_then(|s| s.code()))),
399 }
400 }
401}
402
403impl OnResize for Pty {
404 fn on_resize(&mut self, window_size: WindowSize) {
409 let win = window_size.to_winsize();
410
411 let res = unsafe { libc::ioctl(self.file.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) };
412
413 if res < 0 {
414 die!("ioctl TIOCSWINSZ failed: {}", Error::last_os_error());
415 }
416 }
417}
418
419pub trait ToWinsize {
421 fn to_winsize(self) -> Winsize;
423}
424
425impl ToWinsize for WindowSize {
426 fn to_winsize(self) -> Winsize {
427 let ws_row = self.num_lines as libc::c_ushort;
428 let ws_col = self.num_cols as libc::c_ushort;
429
430 let ws_xpixel = ws_col * self.cell_width as libc::c_ushort;
431 let ws_ypixel = ws_row * self.cell_height as libc::c_ushort;
432 Winsize { ws_row, ws_col, ws_xpixel, ws_ypixel }
433 }
434}
435
436unsafe fn set_nonblocking(fd: c_int) {
437 let res = unsafe { fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) };
438 assert_eq!(res, 0);
439}
440
441#[test]
442fn test_get_pw_entry() {
443 let mut buf: [i8; 1024] = [0; 1024];
444 let _pw = get_pw_entry(&mut buf).unwrap();
445}