Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ trait TerminalBackend {
}
```

`PtyBackend`는 `portable-pty`와 reader/waiter thread로 로컬 child를 소유하고, `HubBackend`는 daemon hub에 요청만 보낸다. 각 pane은 생성 직후 Unix session 또는 Windows Job Object를 종료 경계로 삼아 `destroy_pane`과 hub 종료가 그 경계 안의 subprocess를 함께 종료한다. 단순 detach·quit·browser disconnect는 pane destroy 경로를 호출하지 않으므로 프로세스를 유지한다. pane id·title·resize·reorder는 즉시 로컬 상태로 확정하지 않고 `Created`, `Resized`, `Reordered`, `Exited` 같은 backend event를 따른다. `drain_events`는 보고만 하며 `Exited`를 받은 owner가 `destroy_pane`을 호출해 자원을 회수한다. VT parsing은 두 backend 모두 client-side `PaneEmulator`가 담당한다. pane child의 환경은 daemon이 상속한 값이 아니라 pane이 실제로 렌더되는 emulator를 기준으로 맞춘다: `TERM=xterm-256color`, `COLORTERM=truecolor`를 강제하고 `NO_COLOR`는 제거한다. daemon은 agent shell이나 service manager처럼 터미널이 아닌 곳에서 시작될 수 있고, 그런 부모는 자기 자식용으로 `NO_COLOR=1`, `TERM=dumb`를 내보내는 일이 흔하기 때문이다.
`PtyBackend`는 `portable-pty`와 reader/waiter thread로 로컬 child를 소유하고, `HubBackend`는 daemon hub에 요청만 보낸다. Unix에서는 `portable-pty`가 spawn의 pre-exec에서 `setsid()`를 성공시켜 child PID를 session/process-group ID로 확정하므로, attach는 wait/reap 전에 이 경계를 설정해야 한다. 따라서 이미 종료했지만 아직 reap되지 않은 child도 live-process 조회 없이 같은 경계에 붙일 수 있다. 각 pane은 생성 직후 Unix session 또는 Windows Job Object를 종료 경계로 삼아 `destroy_pane`과 hub 종료가 그 경계 안의 subprocess를 함께 종료한다. 단순 detach·quit·browser disconnect는 pane destroy 경로를 호출하지 않으므로 프로세스를 유지한다. pane id·title·resize·reorder는 즉시 로컬 상태로 확정하지 않고 `Created`, `Resized`, `Reordered`, `Exited` 같은 backend event를 따른다. `drain_events`는 보고만 하며 `Exited`를 받은 owner가 `destroy_pane`을 호출해 자원을 회수한다. VT parsing은 두 backend 모두 client-side `PaneEmulator`가 담당한다. pane child의 환경은 daemon이 상속한 값이 아니라 pane이 실제로 렌더되는 emulator를 기준으로 맞춘다: `TERM=xterm-256color`, `COLORTERM=truecolor`를 강제하고 `NO_COLOR`는 제거한다. daemon은 agent shell이나 service manager처럼 터미널이 아닌 곳에서 시작될 수 있고, 그런 부모는 자기 자식용으로 `NO_COLOR=1`, `TERM=dumb`를 내보내는 일이 흔하기 때문이다.

세션 상한은 repository당 PTY 8개, pane 크기 1–500행 × 1–1100열, pane당 reconnect scrollback 256 KiB다. close와 resize는 bounded input queue 밖의 전용 latest-state 경로로 보내 queue 포화에도 마지막 요청을 잃지 않는다.

Expand Down
21 changes: 12 additions & 9 deletions src/platform/process_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! a Job Object, whose kernel membership includes descendants created after
//! the child is assigned.

use anyhow::{Context as _, Result};
use anyhow::Result;
use portable_pty::{Child, MasterPty};
use std::io;

Expand Down Expand Up @@ -40,14 +40,12 @@ impl ProcessTree {
anyhow::bail!("refusing unsafe PTY process id {pid}");
}

// portable-pty calls setsid() before exec, which makes the child a
// session and process-group leader. Check the session while the
// child is alive so a recycled PID can never be mistaken for ours.
let process_group = unsafe { libc::getpgid(pid) };
let session = unsafe { libc::getsid(pid) };
if process_group <= 1 || session <= 1 {
return Err(io::Error::last_os_error()).context("querying PTY process session");
}
// portable-pty calls setsid() before exec, making this PID the
// session and process-group leader. Attach runs immediately after
// spawn, before any wait/reap, so the PID cannot have been reused;
// no live-process lookup is needed for this identity boundary.
let process_group = pid;
let session = pid;
Ok(Self {
process_group,
session,
Expand All @@ -57,6 +55,7 @@ impl ProcessTree {

#[cfg(windows)]
{
use anyhow::Context as _;
use std::os::windows::io::{AsRawHandle, FromRawHandle};
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::JobObjects::{
Expand Down Expand Up @@ -262,3 +261,7 @@ mod tests {
assert_eq!(parse_proc_stat(stat), Some((123, 122, 122)));
}
}

#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
#[path = "process_tree_tests.rs"]
mod pty_tests;
38 changes: 38 additions & 0 deletions src/platform/process_tree_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use super::ProcessTree;
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem as _};

#[test]
fn attaches_to_an_exited_but_unreaped_pty_child() {
let pair = NativePtySystem::default()
.openpty(PtySize::default())
.expect("open a real PTY");
let mut command = CommandBuilder::new("sh");
command.arg("-c");
command.arg("exit 0");
let mut child = pair
.slave
.spawn_command(command)
.expect("spawn an exited-child fixture");
drop(pair.slave);

let pid = child.process_id().expect("PTY child exposes a PID") as libc::pid_t;
let mut status = std::mem::MaybeUninit::<libc::siginfo_t>::zeroed();
// WNOWAIT leaves the exited child waitable for the cleanup wait below.
let wait_result = unsafe {
libc::waitid(
libc::P_PID,
pid as libc::id_t,
status.as_mut_ptr(),
libc::WEXITED | libc::WNOWAIT,
)
};
assert_eq!(wait_result, 0, "waitid(WEXITED | WNOWAIT)");

let tree = ProcessTree::attach(&*child, &*pair.master).expect("attach exited child");
assert_eq!(tree.process_group, pid);
assert_eq!(tree.session, pid);
assert_eq!(tree.tty, pair.master.as_raw_fd());

let exit = child.wait().expect("reap the exited child");
assert!(exit.success());
}
Loading