remove tui socket stuff

This commit is contained in:
Ryan Ragona
2025-04-26 10:03:24 -07:00
parent 786c81d706
commit 9f10ec53b6
5 changed files with 16 additions and 451 deletions
+6 -38
View File
@@ -633,14 +633,11 @@ name = "codex-session"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"chrono",
"clap 4.5.37",
"codex-core",
"codex-exec",
"codex-repl",
"codex-tui",
"crossterm 0.27.0",
"dirs 5.0.1",
"libc",
"names",
@@ -666,7 +663,7 @@ dependencies = [
"codex-ansi-escape",
"codex-core",
"color-eyre",
"crossterm 0.28.1",
"crossterm",
"ratatui",
"shlex",
"tokio",
@@ -792,22 +789,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.9.0",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.28.1"
@@ -816,7 +797,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.0",
"crossterm_winapi",
"mio 1.0.3",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
@@ -2158,18 +2139,6 @@ dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.3"
@@ -2754,7 +2723,7 @@ dependencies = [
"bitflags 2.9.0",
"cassowary",
"compact_str",
"crossterm 0.28.1",
"crossterm",
"indoc",
"instability",
"itertools 0.13.0",
@@ -3275,8 +3244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio 0.8.11",
"mio 1.0.3",
"mio",
"signal-hook",
]
@@ -3752,7 +3720,7 @@ dependencies = [
"backtrace",
"bytes",
"libc",
"mio 1.0.3",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -4001,7 +3969,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
"crossterm 0.28.1",
"crossterm",
"ratatui",
"unicode-width 0.2.0",
]
-3
View File
@@ -26,10 +26,8 @@ tracing = { version = "0.1.41", features = ["log"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
# --- additions for PTY/TUI support ---
bytes = "1.5"
# Raw terminal handling when attaching to TUI sessions
crossterm = "0.27"
# PTY helpers (unix only)
@@ -50,7 +48,6 @@ nix = { version = "0.27", default-features = false, features = ["process", "sign
# Re-use the codex-exec library for its CLI definition
codex_exec = { package = "codex-exec", path = "../exec" }
codex_repl = { package = "codex-repl", path = "../repl" }
codex_tui = { package = "codex-tui", path = "../tui" }
[dev-dependencies]
tempfile = "3"
+4 -178
View File
@@ -56,7 +56,6 @@ impl Cli {
Commands::Delete(x) => x.run().await,
Commands::Logs(x) => x.run().await,
Commands::List(x) => x.run().await,
Commands::Mux(x) => x.run().await,
}
}
}
@@ -74,9 +73,7 @@ enum Commands {
/// List all known sessions.
List(ListCmd),
/// Internal helper process: PTY multiplexer daemon (hidden).
#[command(hide = true, name = "__mux")]
Mux(MuxCmd),
// (previous mux variant removed)
}
// -----------------------------------------------------------------------------
@@ -90,8 +87,6 @@ enum AgentKind {
/// Interactive Read-Eval-Print-Loop agent.
Repl(ReplCreateCmd),
/// Full-screen Terminal User Interface agent.
Tui(TuiCreateCmd),
}
#[derive(Args)]
@@ -116,11 +111,6 @@ pub struct ReplCreateCmd {
repl_cli: codex_repl::Cli,
}
#[derive(Args)]
pub struct TuiCreateCmd {
#[clap(flatten)]
tui_cli: codex_tui::Cli,
}
impl CreateCmd {
pub async fn run(self) -> Result<()> {
@@ -146,12 +136,6 @@ impl CreateCmd {
let preview = cmd.repl_cli.prompt.as_ref().map(|p| truncate_preview(p));
(child.id().unwrap_or_default(), preview, store::SessionKind::Repl)
}
AgentKind::Tui(cmd) => {
let args = build_tui_args(&cmd.tui_cli);
let child = spawn::spawn_tui(&paths, &args).await?;
let preview = cmd.tui_cli.prompt.as_ref().map(|p| truncate_preview(p));
(child.id().unwrap_or_default(), preview, store::SessionKind::Tui)
}
};
// Persist metadata **after** the process has been spawned so we can record its PID.
@@ -169,29 +153,7 @@ impl CreateCmd {
}
}
// -----------------------------------------------------------------------------
// internal mux helper sub-command (hidden)
#[derive(Args)]
pub struct MuxCmd {
/// Raw PTY master file descriptor passed from the parent process.
#[arg(long)]
fd: i32,
/// Path to the Unix-domain socket that clients attach to.
#[arg(long)]
sock: std::path::PathBuf,
/// Path to the binary stdout log file.
#[arg(long)]
log: std::path::PathBuf,
}
impl MuxCmd {
pub async fn run(self) -> Result<()> {
crate::spawn::mux_main(self.fd, self.sock, self.log).await
}
}
// (mux helper removed)
fn truncate_preview(p: &str) -> String {
let slice: String = p.chars().take(40).collect();
@@ -315,61 +277,6 @@ fn build_repl_args(cli: &codex_repl::Cli) -> Vec<String> {
// For the first implementation we forward only a minimal subset of options that
// are already handled in the REPL helper above. Future work can extend this
// with the full flag surface.
fn build_tui_args(cli: &codex_tui::Cli) -> Vec<String> {
let mut args = Vec::new();
// Positional prompt argument (optional) – must be last.
if let Some(model) = &cli.model {
args.push("--model".into());
args.push(model.clone());
}
for img in &cli.images {
args.push("--image".into());
args.push(img.to_string_lossy().into_owned());
}
if cli.skip_git_repo_check {
args.push("--skip-git-repo-check".into());
}
if cli.disable_response_storage {
args.push("--disable-response-storage".into());
}
// Approval + sandbox policies
args.push("--ask-for-approval".into());
args.push(match cli.approval_policy {
codex_core::ApprovalModeCliArg::OnFailure => "on-failure".into(),
codex_core::ApprovalModeCliArg::UnlessAllowListed => "unless-allow-listed".into(),
codex_core::ApprovalModeCliArg::Never => "never".into(),
});
args.push("--sandbox".into());
args.push(match cli.sandbox_policy {
codex_core::SandboxModeCliArg::NetworkRestricted => "network-restricted".into(),
codex_core::SandboxModeCliArg::FileWriteRestricted => "file-write-restricted".into(),
codex_core::SandboxModeCliArg::NetworkAndFileWriteRestricted =>
"network-and-file-write-restricted".into(),
codex_core::SandboxModeCliArg::DangerousNoRestrictions =>
"dangerous-no-restrictions".into(),
});
// Convenience flags
if cli.full_auto {
args.push("--full-auto".into());
}
if cli.suggest {
args.push("--suggest".into());
}
if let Some(prompt) = &cli.prompt {
args.push(prompt.clone());
}
args
}
// -----------------------------------------------------------------------------
// attach
@@ -389,18 +296,7 @@ impl AttachCmd {
let id = store::resolve_selector(&self.id)?;
let paths = store::paths_for(&id)?;
// Load meta in order to decide which attach strategy to use.
let meta_bytes = std::fs::read(&paths.meta)?;
let meta: store::SessionMeta = serde_json::from_slice(&meta_bytes)?;
match meta.kind {
store::SessionKind::Exec | store::SessionKind::Repl => {
self.attach_line_oriented(&id, &paths).await
}
store::SessionKind::Tui => {
self.attach_tui(&paths).await
}
}
self.attach_line_oriented(&id, &paths).await
}
// ------------------------------------------------------------------
@@ -457,77 +353,7 @@ impl AttachCmd {
Ok(())
}
// ------------------------------------------------------------------
// TUI attach: raw byte forwarding over unix socket
async fn attach_tui(&self, paths: &store::Paths) -> Result<()> {
#[cfg(unix)]
{
use crossterm::{execute, terminal};
use tokio::io;
use tokio::net::UnixStream;
// RAII guard for raw mode + alternate screen.
struct TermGuard;
impl TermGuard {
fn new() -> anyhow::Result<Self> {
execute!(std::io::stdout(), terminal::EnterAlternateScreen)?;
terminal::enable_raw_mode()?;
Ok(Self)
}
}
impl Drop for TermGuard {
fn drop(&mut self) {
let _ = terminal::disable_raw_mode();
let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
}
}
let _term = TermGuard::new()?;
let sock_path = paths.dir.join("sock");
if !sock_path.exists() {
anyhow::bail!(
"tui session socket not found ({}). Is the session fully initialised?",
sock_path.display()
);
}
// Connect to session socket.
let stream = UnixStream::connect(&sock_path).await?;
let (mut reader, mut writer) = stream.into_split();
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let mut to_stdout = tokio::spawn(async move {
let _ = io::copy(&mut reader, &mut stdout).await;
});
let mut to_socket = tokio::spawn(async move {
let _ = io::copy(&mut stdin, &mut writer).await;
});
// Wait for either direction, then abort the other.
tokio::select! {
_ = &mut to_stdout => {
to_socket.abort();
}
_ = &mut to_socket => {
to_stdout.abort();
}
}
let _ = to_stdout.await;
let _ = to_socket.await;
Ok(())
}
#[cfg(not(unix))]
{
anyhow::bail!("tui sessions are only supported on Unix at the moment");
}
}
// (TUI attach removed)
}
// -----------------------------------------------------------------------------
+6 -230
View File
@@ -1,20 +1,12 @@
//! Spawn detached Codex agent processes (exec, repl, tui).
//!
//! The *exec* and *repl* helpers reuse the original FIFO/pipe strategy while
//! the new **tui** flavour allocates a pseudo-terminal so the crossterm /
//! ratatui application sees a *real* tty. A small socket fan-out forwards raw
//! bytes between the PTY and every `codex-session attach` client.
//! Spawn detached Codex agent processes for exec and repl sessions.
use crate::store::Paths;
use anyhow::{Context, Result};
use std::fs::OpenOptions;
use tokio::process::{Child, Command};
#[cfg(unix)]
use std::os::unix::process::CommandExt; // for pre_exec
// -----------------------------------------------------------------------------
// exec – non-interactive batch agent (stdin = /dev/null)
// exec – non-interactive batch agent
pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result<Child> {
#[cfg(unix)]
@@ -37,7 +29,6 @@ pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result<Child> {
.stdout(stdout)
.stderr(stderr);
// Detach session so the child is not killed with the parent.
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
@@ -76,12 +67,12 @@ pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result<Child> {
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
let child = cmd.spawn().context("failed to spawn codex-exec")?;
return Ok(child);
Ok(child)
}
}
// -----------------------------------------------------------------------------
// repl – interactive but **line-oriented** (FIFO for stdin)
// repl – interactive FIFO stdin
pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result<Child> {
#[cfg(unix)]
@@ -89,10 +80,8 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result<Child> {
use std::io;
use std::os::unix::ffi::OsStrExt;
// Ensure the FIFO exists (create with 600 permissions).
if !paths.stdin.exists() {
let c_path = std::ffi::CString::new(paths.stdin.as_os_str().as_bytes()).unwrap();
// SAFETY: libc call, check return value.
let res = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
if res != 0 {
let err = std::io::Error::last_os_error();
@@ -102,8 +91,6 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result<Child> {
}
}
// Open the FIFO read-write so `open()` does **not** block even though
// no external writer is connected yet.
let stdin = OpenOptions::new()
.read(true)
.write(true)
@@ -135,223 +122,12 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result<Child> {
}
let child = cmd.spawn().context("failed to spawn codex-repl")?;
return Ok(child);
Ok(child)
}
#[cfg(windows)]
{
anyhow::bail!("codex-repl sessions are not yet supported on Windows");
anyhow::bail!("codex-repl sessions are not supported on Windows yet");
}
}
// -----------------------------------------------------------------------------
// tui – full terminal UI (PTY + socket fan-out)
use bytes::Bytes;
#[cfg(unix)]
use {
nix::unistd::dup,
std::os::unix::io::{FromRawFd, IntoRawFd, RawFd},
};
/// Spawn `codex-tui` inside a pseudo-terminal and start the background
/// multiplexer so future `attach` commands can talk to it.
pub async fn spawn_tui(paths: &Paths, tui_args: &[String]) -> Result<Child> {
#[cfg(unix)]
{
use std::io;
// 1. PTY allocation ---------------------------------------------------
let pty = nix::pty::openpty(None, None).context("openpty failed")?;
let slave_fd: RawFd = pty.slave.into_raw_fd();
let master_fd: RawFd = pty.master.into_raw_fd();
// Ensure master_fd is inheritable (clear FD_CLOEXEC)
{
use nix::fcntl::{fcntl, FcntlArg, FdFlag};
let _ = fcntl(master_fd, FcntlArg::F_SETFD(FdFlag::empty()));
}
// 2. Spawn codex-tui --------------------------------------------------
let make_stdio = |fd: RawFd| unsafe { std::process::Stdio::from_raw_fd(fd) };
let stdin = make_stdio(dup(slave_fd)?);
let stdout = make_stdio(dup(slave_fd)?);
let stderr = make_stdio(slave_fd);
let mut tui_cmd = Command::new("codex-tui");
tui_cmd.args(tui_args)
.stdin(stdin)
.stdout(stdout)
.stderr(stderr);
unsafe {
tui_cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
libc::signal(libc::SIGHUP, libc::SIG_IGN);
Ok(())
});
}
let child = tui_cmd.spawn().context("failed to spawn codex-tui")?;
// 3. Spawn mux helper process ---------------------------------------
let sock_path = paths.dir.join("sock");
if sock_path.exists() {
let _ = std::fs::remove_file(&sock_path);
}
let current_exe = std::env::current_exe()?;
let mut mux_cmd = std::process::Command::new(current_exe);
mux_cmd.arg("__mux")
.arg("--fd").arg(format!("{master_fd}"))
.arg("--sock").arg(&sock_path)
.arg("--log").arg(&paths.stdout)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
// Detach mux process (own session)
unsafe {
mux_cmd.pre_exec(|| {
libc::setsid();
Ok(())
});
}
let _ = mux_cmd.spawn().context("failed to spawn mux helper")?;
Ok(child)
}
#[cfg(not(unix))]
{
anyhow::bail!("tui sessions are only supported on Unix right now");
}
}
#[cfg(unix)]
async fn spawn_client(
sock: tokio::net::UnixStream,
pty_write_fd: RawFd,
tx: &tokio::sync::broadcast::Sender<Bytes>,
) {
use tokio::io::AsyncWriteExt;
let (mut s_read, mut s_write) = sock.into_split();
// Clone PTY master *write* side for this client
let pty_write = unsafe {
tokio::fs::File::from_std(std::fs::File::from_raw_fd(pty_write_fd))
};
let mut pty_write = pty_write;
// subscribe
let mut rx = tx.subscribe();
// socket → pty
let to_pty = tokio::spawn(async move {
tokio::io::copy(&mut s_read, &mut pty_write).await.ok();
});
// pty broadcast → socket
let from_pty = tokio::spawn(async move {
while let Ok(bytes) = rx.recv().await {
if s_write.write_all(&bytes).await.is_err() {
break;
}
}
});
let _ = tokio::join!(to_pty, from_pty);
}
/// Actual multiplexer event loop that runs inside the forked daemon process.
#[cfg(unix)]
#[cfg(unix)]
pub async fn mux_main(master_fd: RawFd, sock_path: std::path::PathBuf, stdout_log: std::path::PathBuf) -> anyhow::Result<()> {
use tokio::{net::UnixListener, sync::broadcast};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Bind socket (should succeed; stale already removed).
let listener = match UnixListener::bind(&sock_path) {
Ok(l) => l,
Err(e) => {
eprintln!("socket bind failed: {e}");
return Ok(());
}
};
// Async read handle for PTY master.
let master_read = unsafe {
tokio::fs::File::from_std(std::fs::File::from_raw_fd(dup(master_fd).expect("dup master")))
};
// binary log file
let mut log_file = match tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&stdout_log)
.await
{
Ok(f) => f,
Err(e) => {
eprintln!("log open failed: {e}");
return Ok(());
}
};
let (tx, _) = broadcast::channel::<Bytes>(64);
// Reader task
let tx_read = tx.clone();
tokio::spawn(async move {
let mut buf = [0u8; 4096];
let mut r = master_read;
loop {
match r.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
let _ = log_file.write_all(&buf[..n]).await;
let _ = tx_read.send(Bytes::copy_from_slice(&buf[..n]));
}
Err(e) => {
eprintln!("pty read error: {e}");
break;
}
}
}
});
// Accept-loop
loop {
match listener.accept().await {
Ok((sock, _)) => {
match dup(master_fd) {
Ok(fd) => {
let tx_c = tx.clone();
tokio::spawn(async move {
spawn_client(sock, fd, &tx_c).await;
});
}
Err(e) => eprintln!("dup failed: {e}"),
}
}
Err(e) => {
eprintln!("accept error: {e}");
break;
}
}
}
Ok(())
}
#[cfg(not(unix))]
pub async fn mux_main(_fd: i32, _sock: std::path::PathBuf, _log: std::path::PathBuf) -> anyhow::Result<()> {
anyhow::bail!("tui sessions are only supported on unix");
}
-2
View File
@@ -59,8 +59,6 @@ pub enum SessionKind {
Exec,
/// Line-oriented interactive session – `codex-repl`.
Repl,
/// Full terminal-UI session (crossterm / ratatui) – `codex-tui`.
Tui,
}
impl Default for SessionKind {