Skip to content
Open
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ endif()
if(NOT "${GIT_COMMIT}" STREQUAL "")
set(TTYD_VERSION "${TTYD_VERSION}-${GIT_COMMIT}")
endif()
set(TTYD_VERSION "${TTYD_VERSION}-codex-pty1")

if(NOT MSVC)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE")
Expand Down
134 changes: 132 additions & 2 deletions src/pty.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,50 @@ void pty_buf_free(pty_buf_t *buf) {
free(buf);
}

#ifndef _WIN32
#define PTY_RECONNECT_MAX_ATTEMPTS 20
#define PTY_RECONNECT_DELAY_MS 20
#define PTY_STARTUP_RECONNECT_MAX_ATTEMPTS 40
#define PTY_STARTUP_RECONNECT_DELAY_MS 50

static void pty_reconnect_out(pty_process *process);
static void startup_timer_cb(uv_timer_t *timer);
#endif

static void read_cb(uv_stream_t *stream, ssize_t n, const uv_buf_t *buf) {
uv_read_stop(stream);
pty_process *process = (pty_process *) stream->data;
process->paused = true;
if (n <= 0) {
if (n == UV_ENOBUFS || n == 0) return;
if (n == UV_ENOBUFS || n == 0 || n == UV_EAGAIN) {
free(buf->base);
pty_resume(process);
return;
}
#ifndef _WIN32
// The PTY master can briefly return EIO when the slave side is revoked
// and reopened (e.g. login(1) calls vhangup() before re-attaching the
// controlling terminal). libuv reacts to any read error by clearing
// UV_HANDLE_READABLE/WRITABLE on the stream and stopping its io_watcher
// (see uv__read() in libuv's src/unix/stream.c), so the uv_pipe_t is
// permanently unusable even though the underlying kernel fd recovers.
// Schedule a reconnect of process->out instead of tearing the session
// down; only EIO is treated as transient.
if (n == UV_EIO && process_running(process)) {
free(buf->base);
pty_reconnect_out(process);
return;
}
#endif
process->reconnect_attempts = 0;
process->read_cb(process, NULL, true);
goto done;
}
process->output_seen = true;
#ifndef _WIN32
if (process->startup_timer != NULL) uv_timer_stop(process->startup_timer);
#endif
process->reconnect_attempts = 0;
process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false);

done:
Expand Down Expand Up @@ -112,6 +148,16 @@ void process_free(pty_process *process) {
close(process->pty);
uv_thread_join(&process->tid);
#endif
if (process->reconnect_timer != NULL) {
uv_timer_stop(process->reconnect_timer);
uv_close((uv_handle_t *) process->reconnect_timer, close_cb);
process->reconnect_timer = NULL;
}
if (process->startup_timer != NULL) {
uv_timer_stop(process->startup_timer);
uv_close((uv_handle_t *) process->startup_timer, close_cb);
process->startup_timer = NULL;
}
if (process->in != NULL) uv_close((uv_handle_t *) process->in, close_cb);
if (process->out != NULL) uv_close((uv_handle_t *) process->out, close_cb);
if (process->argv != NULL) free(process->argv);
Expand All @@ -124,14 +170,27 @@ void process_free(pty_process *process) {
void pty_pause(pty_process *process) {
if (process == NULL) return;
if (process->paused) return;
process->paused = true;
uv_read_stop((uv_stream_t *) process->out);
}

void pty_resume(pty_process *process) {
if (process == NULL) return;
if (!process->paused) return;
process->out->data = process;
uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
int err = uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
if (err) {
process->paused = true;
#ifndef _WIN32
if (err == UV_EIO || err == UV_EBADF || err == UV_EINVAL) pty_reconnect_out(process);
#endif
return;
}
process->paused = false;
#ifndef _WIN32
if (!process->output_seen && process->startup_timer != NULL && process->startup_reconnect_attempts == 0)
uv_timer_start(process->startup_timer, startup_timer_cb, PTY_STARTUP_RECONNECT_DELAY_MS, 0);
#endif
}

int pty_write(pty_process *process, pty_buf_t *buf) {
Expand Down Expand Up @@ -476,10 +535,19 @@ int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
process->pty = master;
process->pid = pid;
process->paused = true;
process->reconnect_attempts = 0;
process->startup_reconnect_attempts = 0;
process->output_seen = false;
process->read_cb = read_cb;
process->exit_cb = exit_cb;
process->async.data = process;
uv_async_init(process->loop, &process->async, async_cb);
process->reconnect_timer = xmalloc(sizeof(uv_timer_t));
uv_timer_init(process->loop, process->reconnect_timer);
process->reconnect_timer->data = process;
process->startup_timer = xmalloc(sizeof(uv_timer_t));
uv_timer_init(process->loop, process->startup_timer);
process->startup_timer->data = process;
uv_thread_create(&process->tid, wait_cb, process);

return 0;
Expand All @@ -490,4 +558,66 @@ int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
waitpid(pid, NULL, 0);
return status;
}

static bool pty_replace_out(pty_process *process) {
if (process == NULL) return false;

if (process->out != NULL) {
uv_read_stop((uv_stream_t *) process->out);
uv_close((uv_handle_t *) process->out, close_cb);
process->out = NULL;
}

if (!process_running(process)) return false;

process->out = xmalloc(sizeof(uv_pipe_t));
uv_pipe_init(process->loop, process->out, 0);

if (!fd_duplicate(process->pty, process->out)) {
fprintf(stderr, "pty reconnect: fd_duplicate failed for pid %d\n", process->pid);
uv_close((uv_handle_t *) process->out, close_cb);
process->out = NULL;
return false;
}

process->paused = true;
pty_resume(process);
return true;
}

static void startup_timer_cb(uv_timer_t *timer) {
pty_process *process = (pty_process *) timer->data;
if (process == NULL || process->output_seen || !process_running(process)) return;

process->startup_reconnect_attempts++;
if (process->startup_reconnect_attempts > PTY_STARTUP_RECONNECT_MAX_ATTEMPTS) return;

if (!pty_replace_out(process)) return;

if (!process->output_seen && process->startup_reconnect_attempts < PTY_STARTUP_RECONNECT_MAX_ATTEMPTS)
uv_timer_start(process->startup_timer, startup_timer_cb, PTY_STARTUP_RECONNECT_DELAY_MS, 0);
}

static void reconnect_timer_cb(uv_timer_t *timer) {
pty_process *process = (pty_process *) timer->data;
if (process == NULL) return;

pty_replace_out(process);
}

static void pty_reconnect_out(pty_process *process) {
if (process == NULL) return;

process->reconnect_attempts++;
if (process->reconnect_attempts > PTY_RECONNECT_MAX_ATTEMPTS) {
fprintf(stderr, "pty reconnect: giving up on pid %d after %d attempts\n",
process->pid, PTY_RECONNECT_MAX_ATTEMPTS);
// Deliver EOF upstream so the websocket session closes cleanly.
process->read_cb(process, NULL, true);
return;
}

uint64_t delay = PTY_RECONNECT_DELAY_MS;
uv_timer_start(process->reconnect_timer, reconnect_timer_cb, delay, 0);
}
#endif
5 changes: 5 additions & 0 deletions src/pty.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ struct pty_process_ {
pid_t pty;
uv_thread_t tid;
#endif
uv_timer_t *reconnect_timer;
uv_timer_t *startup_timer;
int reconnect_attempts;
int startup_reconnect_attempts;
bool output_seen;
char **argv;
char **envp;
char *cwd;
Expand Down