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
85 changes: 85 additions & 0 deletions src/pty.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,36 @@ void pty_buf_free(pty_buf_t *buf) {
free(buf);
}

#ifndef _WIN32
static void pty_reconnect_out(pty_process *process);
#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;
#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->reconnect_attempts = 0;
process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false);

done:
Expand Down Expand Up @@ -112,6 +134,11 @@ 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->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,12 +151,14 @@ 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->paused = false;
process->out->data = process;
uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
}
Expand Down Expand Up @@ -476,10 +505,14 @@ 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->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;
uv_thread_create(&process->tid, wait_cb, process);

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

// Fixed-interval retry for re-arming process->out after a transient EIO on
// the PTY master. 20 attempts at 20ms = 400ms total, which is plenty for
// login(1) to reopen the slave side after vhangup().
#define PTY_RECONNECT_MAX_ATTEMPTS 20
#define PTY_RECONNECT_DELAY_MS 20

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

// Tear down the poisoned pipe (the underlying dup'd fd is closed by libuv).
if (process->out != NULL) {
uv_read_stop((uv_stream_t *) process->out);
uv_close((uv_handle_t *) process->out, close_cb);
process->out = NULL;
}

// The child may have exited while we were backing off; nothing to reconnect.
if (!process_running(process)) return;

// The kernel master fd (process->pty) is still valid - only libuv's wrapper
// got poisoned. A fresh dup() + uv_pipe_open() gives us a working handle.
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;
}

process->paused = true;
pty_resume(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
2 changes: 2 additions & 0 deletions src/pty.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ struct pty_process_ {
pid_t pty;
uv_thread_t tid;
#endif
uv_timer_t *reconnect_timer;
int reconnect_attempts;
char **argv;
char **envp;
char *cwd;
Expand Down