diff --git a/src/pty.c b/src/pty.c index 25249f478..fb657f6fc 100644 --- a/src/pty.c +++ b/src/pty.c @@ -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: @@ -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); @@ -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); } @@ -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; @@ -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 diff --git a/src/pty.h b/src/pty.h index c3f761c34..dc01ce908 100644 --- a/src/pty.h +++ b/src/pty.h @@ -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;