From e36b7bb27b131ecb6a617f45756ff24b26733a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Sat, 22 Feb 2025 21:47:50 +0100 Subject: [PATCH 01/13] deps: libuv: Implement PTY support --- deps/uv/docs/src/process.rst | 24 +- deps/uv/include/uv.h | 20 +- deps/uv/include/uv/unix.h | 1 + deps/uv/include/uv/win.h | 1 + deps/uv/src/unix/process.c | 218 +++++++++++++--- deps/uv/src/win/process-stdio.c | 10 +- deps/uv/src/win/process.c | 429 ++++++++++++++++++++++++++++++-- deps/uv/test/run-tests.c | 44 ++++ deps/uv/test/test-list.h | 6 + deps/uv/test/test-spawn.c | 280 ++++++++++++++++++++- 10 files changed, 961 insertions(+), 72 deletions(-) diff --git a/deps/uv/docs/src/process.rst b/deps/uv/docs/src/process.rst index f15dcf610f55ca..33f844ec6d6b5d 100644 --- a/deps/uv/docs/src/process.rst +++ b/deps/uv/docs/src/process.rst @@ -92,7 +92,16 @@ Data types * search for the exact file name before trying variants with * extensions like '.exe' or '.cmd'. */ - UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME = (1 << 7) + UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME = (1 << 7), + /* + * Start subprocess with a pseudo terminal. To use this flag, set + * stdio[0] to UV_CREATE_PIPE | UV_READABLE_PIPE and + * stdio[1] to UV_CREATE_PIPE | UV_WRITABLE_PIPE and + * stdio[2] to UV_IGNORE. The first pipe will be the PTY in (write here), + * while the second pipe will be the PTY out (read here). The child will have + * all standard FDs (0, 1 and 2) connected to the PTY as it should. + */ + UV_PROCESS_PTY = (1 << 8) }; .. c:type:: uv_stdio_container_t @@ -264,7 +273,8 @@ API Possible reasons for failing to spawn would include (but not be limited to) the file to execute not existing, not having permissions to use the setuid or setgid specified, or not having enough memory to allocate for the new - process. + process. If the OS does not support PTYs and `UV_PROCESS_PTY` is passed + the error will be `UV_ENOTSUP`. .. versionchanged:: 1.24.0 Added `UV_PROCESS_WINDOWS_HIDE_CONSOLE` and `UV_PROCESS_WINDOWS_HIDE_GUI` flags. @@ -272,6 +282,8 @@ API .. versionchanged:: 1.48.0 Added the `UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME` flag. + .. versionchanged:: 1.51.0 Added the `UV_PROCESS_PTY` flag. + .. c:function:: int uv_process_kill(uv_process_t* handle, int signum) Sends the specified signal to the given process handle. Check the documentation @@ -288,4 +300,12 @@ API .. versionadded:: 1.19.0 +.. c:function:: int uv_pty_resize(uv_process_t* process, unsigned short cols, unsigned short rows) + + For a PTY enabled process, change the size of the terminal. Returns 0 on + success, `UV_EINVAL` when the `process` is not PTY enabled, `UV_ENOTSUP` + when the OS does not support PTYs. + + .. versionadded:: 1.19.0 + .. seealso:: The :c:type:`uv_handle_t` API functions also apply. diff --git a/deps/uv/include/uv.h b/deps/uv/include/uv.h index da06345557bb6c..f25e2f9ee11dd9 100644 --- a/deps/uv/include/uv.h +++ b/deps/uv/include/uv.h @@ -1098,6 +1098,12 @@ typedef struct uv_process_options_s { */ int stdio_count; uv_stdio_container_t* stdio; + /* + * When starting the child with a PTY, these set the initial width and + * height. + */ + unsigned int pty_cols; + unsigned int pty_rows; /* * Libuv can change the child process' user/group id. This happens only when * the appropriate bits are set in the flags fields. This is not supported on @@ -1160,7 +1166,16 @@ enum uv_process_flags { * search for the exact file name before trying variants with * extensions like '.exe' or '.cmd'. */ - UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME = (1 << 7) + UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME = (1 << 7), + /* + * Start subprocess with a pseudo terminal. To use this flag, set + * stdio[0] to UV_CREATE_PIPE | UV_READABLE_PIPE and + * stdio[1] to UV_CREATE_PIPE | UV_WRITABLE_PIPE and + * stdio[2] to UV_IGNORE. The first pipe will be the PTY in (write here), + * while the second pipe will be the PTY out (read here). The child will have + * all standard FDs (0, 1 and 2) connected to the PTY as it should. + */ + UV_PROCESS_PTY = (1 << 8) }; /* @@ -1176,6 +1191,9 @@ struct uv_process_s { UV_EXTERN int uv_spawn(uv_loop_t* loop, uv_process_t* handle, const uv_process_options_t* options); +UV_EXTERN int uv_pty_resize(uv_process_t* process, + unsigned short cols, + unsigned short rows); UV_EXTERN int uv_process_kill(uv_process_t*, int signum); UV_EXTERN int uv_kill(int pid, int signum); UV_EXTERN uv_pid_t uv_process_get_pid(const uv_process_t*); diff --git a/deps/uv/include/uv/unix.h b/deps/uv/include/uv/unix.h index c6ba419da98b39..43b069a332f3d4 100644 --- a/deps/uv/include/uv/unix.h +++ b/deps/uv/include/uv/unix.h @@ -357,6 +357,7 @@ typedef struct { #define UV_PROCESS_PRIVATE_FIELDS \ struct uv__queue queue; \ int status; \ + int pty_fd; #define UV_FS_PRIVATE_FIELDS \ const char *new_path; \ diff --git a/deps/uv/include/uv/win.h b/deps/uv/include/uv/win.h index 7b4ebd4b7b2e85..ebd2fb1abec103 100644 --- a/deps/uv/include/uv/win.h +++ b/deps/uv/include/uv/win.h @@ -610,6 +610,7 @@ typedef struct { int exit_signal; \ HANDLE wait_handle; \ HANDLE process_handle; \ + HANDLE pty_handle; \ volatile char exit_cb_pending; #define UV_FS_PRIVATE_FIELDS \ diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 539e7d9417d3bf..512decec26cf0a 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -31,15 +31,17 @@ #include #include +#include +#include #include #include #include +#include #if defined(__APPLE__) # include # include # include -# include # include # include # include @@ -287,7 +289,11 @@ static void uv__write_errno(int error_fd) { static void uv__process_child_init(const uv_process_options_t* options, int stdio_count, int (*pipes)[2], - int error_fd) { + int error_fd, + int also_close_fd) { + if (also_close_fd >= 0) + uv__close(also_close_fd); + sigset_t signewset; int close_fd; int use_fd; @@ -314,9 +320,6 @@ static void uv__process_child_init(const uv_process_options_t* options, uv__write_errno(error_fd); } - if (options->flags & UV_PROCESS_DETACHED) - setsid(); - /* First duplicate low numbered fds, since it's not safe to duplicate them, * they could get replaced. Example: swapping stdout and stderr; without * this fd 2 (stderr) would be duplicated into fd 1, thus making both @@ -379,6 +382,20 @@ static void uv__process_child_init(const uv_process_options_t* options, uv__close(close_fd); } + if (options->flags & UV_PROCESS_PTY) { + // Put ourself into a new session and process group, making us session + // and process group leader. + if (setsid() < 0) { + uv__write_errno(error_fd); + } + + // Make our dear terminal the controlling terminal. + if (ioctl(STDIN_FILENO, TIOCSCTTY) < 0) + uv__write_errno(error_fd); + } + else if (options->flags & UV_PROCESS_DETACHED) + setsid(); + if (options->cwd != NULL && chdir(options->cwd)) uv__write_errno(error_fd); @@ -820,7 +837,8 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options, int stdio_count, int (*pipes)[2], int error_fd, - pid_t* pid) { + pid_t* pid, + int close_fd) { sigset_t signewset; sigset_t sigoldset; @@ -842,7 +860,7 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options, if (*pid == 0) { /* Fork succeeded, in the child process */ - uv__process_child_init(options, stdio_count, pipes, error_fd); + uv__process_child_init(options, stdio_count, pipes, error_fd, close_fd); abort(); } @@ -862,7 +880,8 @@ static int uv__spawn_and_init_child( const uv_process_options_t* options, int stdio_count, int (*pipes)[2], - pid_t* pid) { + pid_t* pid, + int close_fd) { int signal_pipe[2] = { -1, -1 }; int status; int err; @@ -870,33 +889,34 @@ static int uv__spawn_and_init_child( ssize_t r; #if defined(__APPLE__) - uv_once(&posix_spawn_init_once, uv__spawn_init_posix_spawn); - - /* Special child process spawn case for macOS Big Sur (11.0) onwards - * - * Big Sur introduced a significant performance degradation on a call to - * fork/exec when the process has many pages mmaped in with MAP_JIT, like, say - * a javascript interpreter. Electron-based applications, for example, - * are impacted; though the magnitude of the impact depends on how much the - * app relies on subprocesses. - * - * On macOS, though, posix_spawn is implemented in a way that does not - * exhibit the problem. This block implements the forking and preparation - * logic with posix_spawn and its related primitives. It also takes advantage of - * the macOS extension POSIX_SPAWN_CLOEXEC_DEFAULT that makes impossible to - * leak descriptors to the child process. */ - err = uv__spawn_and_init_child_posix_spawn(options, - stdio_count, - pipes, - pid, - &posix_spawn_fncs); - - /* The posix_spawn flow will return UV_ENOSYS if any of the posix_spawn_x_np - * non-standard functions is both _needed_ and _undefined_. In those cases, - * default back to the fork/execve strategy. For all other errors, just fail. */ - if (err != UV_ENOSYS) - return err; - + if (!(options->flags & UV_PROCESS_PTY)) { + uv_once(&posix_spawn_init_once, uv__spawn_init_posix_spawn); + + /* Special child process spawn case for macOS Big Sur (11.0) onwards + * + * Big Sur introduced a significant performance degradation on a call to + * fork/exec when the process has many pages mmaped in with MAP_JIT, like, say + * a javascript interpreter. Electron-based applications, for example, + * are impacted; though the magnitude of the impact depends on how much the + * app relies on subprocesses. + * + * On macOS, though, posix_spawn is implemented in a way that does not + * exhibit the problem. This block implements the forking and preparation + * logic with posix_spawn and its related primitives. It also takes advantage of + * the macOS extension POSIX_SPAWN_CLOEXEC_DEFAULT that makes impossible to + * leak descriptors to the child process. */ + err = uv__spawn_and_init_child_posix_spawn(options, + stdio_count, + pipes, + pid, + &posix_spawn_fncs); + + /* The posix_spawn flow will return UV_ENOSYS if any of the posix_spawn_x_np + * non-standard functions is both _needed_ and _undefined_. In those cases, + * default back to the fork/execve strategy. For all other errors, just fail. */ + if (err != UV_ENOSYS) + return err; + } #endif /* This pipe is used by the parent to wait until @@ -926,7 +946,7 @@ static int uv__spawn_and_init_child( /* Acquire write lock to prevent opening new fds in worker threads */ uv_rwlock_wrlock(&loop->cloexec_lock); - err = uv__spawn_and_init_child_fork(options, stdio_count, pipes, signal_pipe[1], pid); + err = uv__spawn_and_init_child_fork(options, stdio_count, pipes, signal_pipe[1], pid, close_fd); /* Release lock in parent process */ uv_rwlock_wrunlock(&loop->cloexec_lock); @@ -963,6 +983,74 @@ static int uv__spawn_and_init_child( } #endif /* ISN'T TARGET_OS_TV || TARGET_OS_WATCH */ +int uv__pty_resize_fd(int pty_fd, + unsigned short cols, + unsigned short rows) { + struct winsize winp; + winp.ws_col = cols; + winp.ws_row = rows; + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + if (ioctl(pty_fd, TIOCSWINSZ, &winp) < 0) + return UV__ERR(errno); + return 0; +} + +int uv__spawn_make_pty(int *fd_pty, int *fd_tty, int cols, int rows) { + int ret; + int my_errno; + + *fd_pty = posix_openpt(O_RDWR); + if (*fd_pty < 0) + return UV__ERR(errno); + + if (grantpt(*fd_pty) < 0) { + my_errno = UV__ERR(errno); + close(*fd_pty); + return my_errno; + } + + if (unlockpt(*fd_pty) < 0) { + my_errno = UV__ERR(errno); + close(*fd_pty); + return my_errno; + } + + int path_tty_size = 40; + char *path_tty = uv__malloc(path_tty_size * sizeof(char)); + // Apple and linux both have ptsname_r. + // Use TIOCGPTPEER. (see man ioctl_tty) Where is that available? + // There is no ptsname_r on OpenBSD. + while ((ret = ptsname_r(*fd_pty, path_tty, path_tty_size)) == ERANGE) { + path_tty_size *= 2; + path_tty = uv__realloc(path_tty, path_tty_size * sizeof(char)); + } + if (ret != 0) { + my_errno = UV__ERR(errno); + uv__free(path_tty); + close(*fd_pty); + return my_errno; + } + + *fd_tty = open(path_tty, O_RDWR | O_NOCTTY); + if (*fd_tty < 0) { + my_errno = UV__ERR(errno); + uv__free(path_tty); + close(*fd_pty); + return my_errno; + } + + uv__free(path_tty); + + if ((my_errno = uv__pty_resize_fd(*fd_pty, cols, rows)) != 0) { + close(*fd_pty); + close(*fd_tty); + return my_errno; + } + + return 0; +} + int uv_spawn(uv_loop_t* loop, uv_process_t* process, const uv_process_options_t* options) { @@ -977,6 +1065,19 @@ int uv_spawn(uv_loop_t* loop, int err; int exec_errorno; int i; + int fd_tty; + + if (options->flags & UV_PROCESS_PTY) { + if (options->stdio[0].flags != (UV_CREATE_PIPE | UV_READABLE_PIPE) || + options->stdio[0].data.stream->type != UV_NAMED_PIPE || + options->stdio[1].flags != (UV_CREATE_PIPE | UV_WRITABLE_PIPE) || + options->stdio[1].data.stream->type != UV_NAMED_PIPE || + options->stdio[2].flags != UV_IGNORE) + return UV_EINVAL; + if (options->pty_rows == 0 | + options->pty_cols == 0) + return UV_EINVAL; + } assert(options->file != NULL); assert(!(options->flags & ~(UV_PROCESS_DETACHED | @@ -986,11 +1087,13 @@ int uv_spawn(uv_loop_t* loop, UV_PROCESS_WINDOWS_HIDE | UV_PROCESS_WINDOWS_HIDE_CONSOLE | UV_PROCESS_WINDOWS_HIDE_GUI | - UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS))); + UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS | + UV_PROCESS_PTY))); uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS); uv__queue_init(&process->queue); process->status = 0; + process->pty_fd = -1; stdio_count = options->stdio_count; if (stdio_count < 3) @@ -1009,18 +1112,33 @@ int uv_spawn(uv_loop_t* loop, pipes[i][1] = -1; } - for (i = 0; i < options->stdio_count; i++) { + for (i = (options->flags & UV_PROCESS_PTY) ? 3 : 0; i < options->stdio_count; i++) { err = uv__process_init_stdio(options->stdio + i, pipes[i]); if (err) goto error; } + if (options->flags & UV_PROCESS_PTY) { + if ((err = uv__spawn_make_pty(&process->pty_fd, &fd_tty, options->pty_cols, options->pty_rows)) != 0) + goto error; + + pipes[0][1] = fd_tty; + pipes[1][1] = fd_tty; + pipes[2][1] = fd_tty; + pipes[0][0] = process->pty_fd; + if ((pipes[1][0] = dup(process->pty_fd)) < 0) { + err = UV__ERR(errno); + goto error; + } + } + + #ifdef UV_USE_SIGCHLD uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD); #endif /* Spawn the child */ - exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, &pid); + exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, &pid, process->pty_fd); #if 0 /* This runs into a nodejs issue (it expects initialized streams, even if the @@ -1056,7 +1174,19 @@ int uv_spawn(uv_loop_t* loop, uv__handle_start(process); } - for (i = 0; i < options->stdio_count; i++) { + // We could special case this (do it as part of the below for loop) if it's ok for the pipe to be non-blocking. + // TODO: Validate this. + if (options->flags & UV_PROCESS_PTY) { + err = uv__close(fd_tty); + + if ((err = uv_pipe_open((uv_pipe_t *)(options->stdio[0].data.stream), pipes[0][0])) != 0) + printf("uv_pipe_open 0 ret: %i\n", err); + + if ((err = uv_pipe_open((uv_pipe_t *)(options->stdio[1].data.stream), pipes[1][0])) != 0) + printf("uv_pipe_open 1 ret: %i\n", err); + } + + for (i = (options->flags & UV_PROCESS_PTY) ? 3 : 0; i < options->stdio_count; i++) { err = uv__process_open_stream(options->stdio + i, pipes[i]); if (err == 0) continue; @@ -1122,3 +1252,11 @@ void uv__process_close(uv_process_t* handle) { uv_signal_stop(&handle->loop->child_watcher); #endif } + +int uv_pty_resize(uv_process_t* process, + unsigned short cols, + unsigned short rows) { + if (process->pty_fd == -1) + return UV_EINVAL; + return uv__pty_resize_fd(process->pty_fd, cols, rows); +} diff --git a/deps/uv/src/win/process-stdio.c b/deps/uv/src/win/process-stdio.c index 181db92ea30d45..2fbb07f1eb4f35 100644 --- a/deps/uv/src/win/process-stdio.c +++ b/deps/uv/src/win/process-stdio.c @@ -192,12 +192,20 @@ int uv__stdio_create(uv_loop_t* loop, /* Prepopulate the buffer with INVALID_HANDLE_VALUE handles so we can clean * up on failure. */ CHILD_STDIO_COUNT(buffer) = count; + for (i = 0; i < count; i++) { CHILD_STDIO_CRT_FLAGS(buffer, i) = 0; memset(CHILD_STDIO_HANDLE(buffer, i), 0xFF, sizeof(HANDLE)); } - for (i = 0; i < count; i++) { + /* In PTY mode the STDIO handles are connected via a separate mechanism. Skip + * them here. */ + if (options->flags & UV_PROCESS_PTY) + i = 3; + else + i = 0; + + for (; i < count; i++) { uv_stdio_container_t fdopt; if (i < options->stdio_count) { fdopt = options->stdio[i]; diff --git a/deps/uv/src/win/process.c b/deps/uv/src/win/process.c index 45e85ed9613900..0ee34b23763d21 100644 --- a/deps/uv/src/win/process.c +++ b/deps/uv/src/win/process.c @@ -38,6 +38,17 @@ #define SIGKILL 9 +#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE +#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \ + ProcThreadAttributeValue(22, FALSE, TRUE, FALSE) +#endif + +typedef VOID* HPCON; +typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE hOut, DWORD dwFlags, HPCON* phpcon); +typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize); +typedef HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc); +typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc); +typedef HRESULT (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc); typedef struct env_var { const WCHAR* const wide; @@ -135,6 +146,7 @@ static void uv__process_init(uv_loop_t* loop, uv_process_t* handle) { handle->wait_handle = INVALID_HANDLE_VALUE; handle->process_handle = INVALID_HANDLE_VALUE; handle->exit_cb_pending = 0; + handle->pty_handle = NULL; UV_REQ_INIT(&handle->exit_req, UV_PROCESS_EXIT); handle->exit_req.data = handle; @@ -814,6 +826,35 @@ static void CALLBACK exit_wait_callback(void* data, BOOLEAN didTimeout) { POST_COMPLETION_FOR_REQ(loop, &process->exit_req); } +typedef struct { + uv_work_t req; + HANDLE pty_handle; +} uv__pty_close_pty_work; + +void pty_close_pty_cb(uv_work_t *req) { + HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); + // Error loading kernel32.dll: (error code %i) + if (!hLibrary) + return; + // return uv_translate_sys_error(GetLastError()); + + PFNCLOSEPSEUDOCONSOLE pfnClose = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"ClosePseudoConsole"); + if (!pfnClose) + return; +// int err = GetLastError(); +// if (err == ERROR_PROC_NOT_FOUND) +// return UV_ENOTSUP; +// else +// return uv_translate_sys_error(err); + + uv__pty_close_pty_work *close_req = (uv__pty_close_pty_work *)req->data; + pfnClose(close_req->pty_handle); + FreeLibrary(hLibrary); +} + +void pty_close_pty_cleanup_cb(uv_work_t *req, int status) { + uv__free(req); +} /* Called on main thread after a child process has exited. */ void uv__process_proc_exit(uv_loop_t* loop, uv_process_t* handle) { @@ -847,13 +888,19 @@ void uv__process_proc_exit(uv_loop_t* loop, uv_process_t* handle) { exit_code = uv_translate_sys_error(GetLastError()); } + if (handle->pty_handle != NULL) { + uv__pty_close_pty_work *close_pty_work = (uv__pty_close_pty_work*)uv__malloc(sizeof(uv__pty_close_pty_work)); + close_pty_work->req.data = (void*)close_pty_work; + close_pty_work->pty_handle = handle->pty_handle; + uv_queue_work(loop, &(close_pty_work->req), &pty_close_pty_cb, &pty_close_pty_cleanup_cb); + } + /* Fire the exit callback. */ if (handle->exit_cb) { handle->exit_cb(handle, exit_code, handle->exit_signal); } } - void uv__process_close(uv_loop_t* loop, uv_process_t* handle) { uv__handle_closing(handle); @@ -874,7 +921,6 @@ void uv__process_close(uv_loop_t* loop, uv_process_t* handle) { } } - void uv__process_endgame(uv_loop_t* loop, uv_process_t* handle) { assert(!handle->exit_cb_pending); assert(handle->flags & UV_HANDLE_CLOSING); @@ -886,17 +932,187 @@ void uv__process_endgame(uv_loop_t* loop, uv_process_t* handle) { uv__handle_close(handle); } +int uv_pty_resize(uv_process_t* process, + unsigned short cols, + unsigned short rows) { + if (process->pty_handle == NULL) + return UV_EINVAL; + HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); + // Error loading kernel32.dll: (error code %i) + if (!hLibrary) + return uv_translate_sys_error(GetLastError()); + + PFNRESIZEPSEUDOCONSOLE pfnResize = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"ResizePseudoConsole"); + if (!pfnResize) { + int err = GetLastError(); + if (err == ERROR_PROC_NOT_FOUND) + return UV_ENOTSUP; + else + return uv_translate_sys_error(err); + } + + COORD size = {cols, rows}; + HRESULT hr = pfnResize(process->pty_handle, size); + // Failed to resize PTY device: (error code %i) + if (FAILED(hr)) + return uv_translate_sys_error(GetLastError()); + return 0; +} + +typedef struct { + uv_work_t req; + HANDLE forward_handle; + ssize_t nread; + char *buf; +} uv__pty_in_forward_data; + +typedef struct { + HANDLE pty_in_write; + uv_process_t *process; +} uv__pty_stream_forward_data; + +static void pty_stdin_forward_alloc_cb(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + buf->base = (char*)uv__malloc(suggested_size); + buf->len = suggested_size; +} + +/* Forwarding PTY STDIN. + * Step 2: We're on a separate thread now. Retrieve the data to be written and + * write to the sync pipe. + */ +void pty_stdin_forward_write_cb(uv_work_t *req) { + int err = 0; + uv__pty_in_forward_data *forward_data = (uv__pty_in_forward_data *)req->data; + DWORD nwritten; + if (!WriteFile(forward_data->forward_handle, forward_data->buf, forward_data->nread, &nwritten, NULL)) { + err = GetLastError(); + // Can't do much about the error sadly. + } +} + +void pty_stdin_forward_after_write_cb(uv_work_t *req, int status) { + uv__pty_in_forward_data *forward_data = (uv__pty_in_forward_data *)req->data; + uv__free(forward_data); +} + +/* Forwarding PTY STDIN. + * Step 1: Read the async pipe and call uv_queue_work to run the rest on a + * separate thread. + */ +void pty_stdin_forward_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + uv__pty_stream_forward_data *stream_forward_data = (uv__pty_stream_forward_data*)stream->data; + if (stream_forward_data->process->flags & UV_HANDLE_CLOSED || nread == UV_EOF) { + uv_read_stop(stream); + uv_close((uv_handle_t*)stream, NULL); + CloseHandle(stream_forward_data->pty_in_write); + uv__free(stream_forward_data); + } + else if (nread > 0) { + uv__pty_in_forward_data *forward_data = (uv__pty_in_forward_data*)uv__malloc(sizeof(uv__pty_in_forward_data)); + forward_data->req.data = (void*)forward_data; + forward_data->nread = nread; + + forward_data->buf = (char*)uv__malloc(sizeof(char) * nread); + memcpy(forward_data->buf, buf->base, nread); + + forward_data->forward_handle = stream_forward_data->pty_in_write; + + uv_queue_work(stream->loop, &(forward_data->req), &pty_stdin_forward_write_cb, &pty_stdin_forward_after_write_cb); + } + + if (buf->base != NULL) { + uv__free(buf->base); + } +} + +typedef struct { + uv_work_t req; + DWORD nread; + char *buf; + HANDLE pty_out_read; + uv_pipe_t out_write; +} uv__pty_out_data; +const int out_read_buf_len = 512; + +void pty_stdout_read_cb(uv_work_t *req) { + int err = 0; + uv__pty_out_data *out_data = (uv__pty_out_data *)req->data; + out_data->nread = 0; + + if (!ReadFile(out_data->pty_out_read, out_data->buf, out_read_buf_len, &(out_data->nread), NULL)) { + err = GetLastError(); + if (err != ERROR_BROKEN_PIPE) { + // Can't do much about the error sadly. + } + } +} + +void pty_stdout_after_write_cb(uv_write_t *req, int status) { + uv__free(req->data); + uv__free(req); +} + +void pty_free_out_data(uv_handle_t *handle) { + uv__free(handle->data); +} + +void pty_stdout_after_read_cb(uv_work_t *req, int status) { + int err = 0; + uv__pty_out_data *out_data = (uv__pty_out_data *)req->data; + if (out_data->nread > 0) { + uv_buf_t *buf = (uv_buf_t*)uv__malloc(sizeof(uv_buf_t)); + buf->base = (char*)uv__malloc(sizeof(char) * out_data->nread); + buf->len = out_data->nread; + memcpy(buf->base, out_data->buf, out_data->nread); + uv_write_t *write_req = (uv_write_t*)uv__malloc(sizeof(uv_write_t)); + write_req->data = (void*)buf; + + uv_write(write_req, (uv_stream_t*)&(out_data->out_write), buf, 1, pty_stdout_after_write_cb); + + uv_queue_work(req->loop, &(out_data->req), &pty_stdout_read_cb, &pty_stdout_after_read_cb); + } + else if (out_data->nread == 0) { + CloseHandle(out_data->pty_out_read); + out_data->out_write.data = (void*)out_data; + uv__free(out_data->buf); + uv_close((uv_handle_t*)&(out_data->out_write), &pty_free_out_data); + } + else { + // Can't do much about the error sadly. + } +} + +// Copied from https://github.com/microsoft/terminal/blob/c4fbb58f69b0a5cc86c245505311d0a0b3cc1399/src/winconpty/winconpty.cpp#L532-L559 +typedef struct { + HANDLE hSignal; + HANDLE hPtyReference; + HANDLE hConPtyProcess; +} uv__pseudo_console; +HRESULT uv__release_pseudo_console(HPCON hPC) { + uv__pseudo_console *pPty = (uv__pseudo_console*)hPC; + if (pPty == NULL) + return E_INVALIDARG; + + if (pPty->hPtyReference != INVALID_HANDLE_VALUE && pPty->hPtyReference != NULL) { + CloseHandle(pPty->hPtyReference); + pPty->hPtyReference = NULL; + } + + return S_OK; +} int uv_spawn(uv_loop_t* loop, uv_process_t* process, const uv_process_options_t* options) { int i; int err = 0; + WCHAR* path = NULL, *alloc_path = NULL; BOOL result; WCHAR* application_path = NULL, *application = NULL, *arguments = NULL, *env = NULL, *cwd = NULL; - STARTUPINFOW startup; + STARTUPINFOEXW startupex; + ZeroMemory(&startupex, sizeof(startupex)); PROCESS_INFORMATION info; DWORD process_flags, cwd_len; BYTE* child_stdio_buffer; @@ -914,6 +1130,21 @@ int uv_spawn(uv_loop_t* loop, return UV_EINVAL; } + if (options->flags & UV_PROCESS_PTY) { + if (options->stdio[0].flags != (UV_CREATE_PIPE | UV_READABLE_PIPE) || + options->stdio[0].data.stream->type != UV_NAMED_PIPE || + options->stdio[1].flags != (UV_CREATE_PIPE | UV_WRITABLE_PIPE) || + options->stdio[1].data.stream->type != UV_NAMED_PIPE || + options->stdio[2].flags != UV_IGNORE) + return UV_EINVAL; + if (options->flags & (UV_PROCESS_WINDOWS_HIDE | + UV_PROCESS_WINDOWS_HIDE_CONSOLE)) + return UV_EINVAL; + if (options->pty_rows == 0 || + options->pty_cols == 0) + return UV_EINVAL; + } + assert(options->file != NULL); assert(!(options->flags & ~(UV_PROCESS_DETACHED | UV_PROCESS_SETGID | @@ -922,7 +1153,8 @@ int uv_spawn(uv_loop_t* loop, UV_PROCESS_WINDOWS_HIDE | UV_PROCESS_WINDOWS_HIDE_CONSOLE | UV_PROCESS_WINDOWS_HIDE_GUI | - UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS))); + UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS | + UV_PROCESS_PTY))); err = uv__utf8_to_utf16_alloc(options->file, &application); if (err) @@ -1002,10 +1234,6 @@ int uv_spawn(uv_loop_t* loop, } } - err = uv__stdio_create(loop, options, &child_stdio_buffer); - if (err) - goto done; - application_path = search_path(application, cwd, path, @@ -1016,18 +1244,123 @@ int uv_spawn(uv_loop_t* loop, goto done; } - startup.cb = sizeof(startup); - startup.lpReserved = NULL; - startup.lpDesktop = NULL; - startup.lpTitle = NULL; - startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + startupex.StartupInfo.cb = sizeof(startupex); + startupex.StartupInfo.lpReserved = NULL; + startupex.StartupInfo.lpDesktop = NULL; + startupex.StartupInfo.lpTitle = NULL; + startupex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; + + err = uv__stdio_create(loop, options, &child_stdio_buffer); + if (err) + goto done; + + if (options->flags & UV_PROCESS_PTY) { + HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); + if (!hLibrary) { + // Error loading kernel32.dll: (error code %i) + err = GetLastError(); + goto done; + } + + PFNCREATEPSEUDOCONSOLE pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"CreatePseudoConsole"); + if (!pfnCreate) { + err = GetLastError(); + if (err == ERROR_PROC_NOT_FOUND) { + err = UV_ENOTSUP; + goto done_uv; + } + else { + goto done; + } + } + + /* Forwarding PTY STDIN. + * We read an asynchronous pipe and in a separate thread write to a + * synchronous pipe. We need to do this, because older versions of ConPTY + * do not support async pipes. + */ + uv_pipe_t* in_write_pipe = (uv_pipe_t*) options->stdio[0].data.stream; + HANDLE in_read = INVALID_HANDLE_VALUE; + err = uv__create_stdio_pipe_pair(loop, + in_write_pipe, + &in_read, + options->stdio[0].flags); + if (err) + goto done; + + int in_read_fd = uv_open_osfhandle(in_read); + uv_pipe_t* in_read_pipe = (uv_pipe_t*)uv__malloc(sizeof(uv_pipe_t)); + uv_pipe_init(loop, in_read_pipe, 0); + uv_pipe_open(in_read_pipe, in_read_fd); + + HANDLE pty_in_read = INVALID_HANDLE_VALUE; + HANDLE pty_in_write = INVALID_HANDLE_VALUE; + if (!CreatePipe(&pty_in_read, &pty_in_write, NULL, 0)) { + err = GetLastError(); + goto done; + } + + uv__pty_stream_forward_data* stream_forward_data = (void*)uv__malloc(sizeof(uv__pty_stream_forward_data)); + stream_forward_data->pty_in_write = pty_in_write; + stream_forward_data->process = process; + in_read_pipe->data = (void*)stream_forward_data; + + uv_read_start((uv_stream_t*)in_read_pipe, pty_stdin_forward_alloc_cb, pty_stdin_forward_read_cb); + + /* Forwarding PTY STDOUT + */ + uv__pty_out_data *out_data = (uv__pty_out_data*)uv__malloc(sizeof(uv__pty_out_data)); + out_data->req.data = (void*)out_data; + out_data->buf = (char*)uv__malloc(sizeof(char) * out_read_buf_len); + + uv_pipe_t* out_read_pipe = (uv_pipe_t*) options->stdio[1].data.stream; + HANDLE out_write = INVALID_HANDLE_VALUE; + err = uv__create_stdio_pipe_pair(loop, + out_read_pipe, + &out_write, + options->stdio[1].flags); + if (err) + goto done; + int out_write_fd = uv_open_osfhandle(out_write); + uv_pipe_init(loop, &(out_data->out_write), 0); + uv_pipe_open(&(out_data->out_write), out_write_fd); + + HANDLE pty_out_write = INVALID_HANDLE_VALUE; + out_data->pty_out_read = INVALID_HANDLE_VALUE; + if (!CreatePipe(&(out_data->pty_out_read), &pty_out_write, NULL, 0)) { + err = GetLastError(); + goto done; + } + + uv_queue_work(loop, &(out_data->req), &pty_stdout_read_cb, &pty_stdout_after_read_cb); - startup.cbReserved2 = uv__stdio_size(child_stdio_buffer); - startup.lpReserved2 = (BYTE*) child_stdio_buffer; + /* Now setup the PTY object + */ + COORD size = {options->pty_cols, options->pty_rows}; - startup.hStdInput = uv__stdio_handle(child_stdio_buffer, 0); - startup.hStdOutput = uv__stdio_handle(child_stdio_buffer, 1); - startup.hStdError = uv__stdio_handle(child_stdio_buffer, 2); + HRESULT hr = pfnCreate(size, pty_in_read, pty_out_write, 0, &process->pty_handle); + if (FAILED(hr)) { + // Failed to create PTY device: (error code %i) + err = hr; + goto done; + } + + FreeLibrary(hLibrary); + CloseHandle(pty_in_read); + CloseHandle(pty_out_write); + + startupex.StartupInfo.hStdInput = NULL; + startupex.StartupInfo.hStdOutput = NULL; + startupex.StartupInfo.hStdError = NULL; + } + else { + startupex.StartupInfo.hStdInput = uv__stdio_handle(child_stdio_buffer, 0); + startupex.StartupInfo.hStdOutput = uv__stdio_handle(child_stdio_buffer, 1); + startupex.StartupInfo.hStdError = uv__stdio_handle(child_stdio_buffer, 2); + } + + startupex.StartupInfo.cbReserved2 = uv__stdio_size(child_stdio_buffer); + startupex.StartupInfo.lpReserved2 = (BYTE*) child_stdio_buffer; process_flags = CREATE_UNICODE_ENVIRONMENT; @@ -1044,9 +1377,9 @@ int uv_spawn(uv_loop_t* loop, if ((options->flags & UV_PROCESS_WINDOWS_HIDE_GUI) || (options->flags & UV_PROCESS_WINDOWS_HIDE)) { /* Use SW_HIDE to avoid any potential process window. */ - startup.wShowWindow = SW_HIDE; + startupex.StartupInfo.wShowWindow = SW_HIDE; } else { - startup.wShowWindow = SW_SHOWDEFAULT; + startupex.StartupInfo.wShowWindow = SW_SHOWDEFAULT; } if (options->flags & UV_PROCESS_DETACHED) { @@ -1064,15 +1397,41 @@ int uv_spawn(uv_loop_t* loop, process_flags |= CREATE_SUSPENDED; } + if (options->flags & UV_PROCESS_PTY) { + process_flags |= EXTENDED_STARTUPINFO_PRESENT; + size_t attr_list_size; + InitializeProcThreadAttributeList(NULL, 1, 0, &attr_list_size); + startupex.lpAttributeList = uv__malloc(attr_list_size); + + if (!InitializeProcThreadAttributeList(startupex.lpAttributeList, 1, 0, + &attr_list_size)) { + // Failed to init proc thread attribute list. (error code %i) + err = GetLastError(); + goto done; + } + + if (!UpdateProcThreadAttribute(startupex.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + process->pty_handle, + sizeof(process->pty_handle), + NULL, + NULL)) { + // Failed to update proc thread attribute list. (error code %i) + err = GetLastError(); + goto done; + } + } + if (!CreateProcessW(application_path, arguments, NULL, NULL, - 1, + TRUE, process_flags, env, cwd, - &startup, + &startupex.StartupInfo, &info)) { /* CreateProcessW failed. */ err = GetLastError(); @@ -1109,6 +1468,31 @@ int uv_spawn(uv_loop_t* loop, } } + if (options->flags & UV_PROCESS_PTY) { + HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); + if (!hLibrary) { + // Error loading kernel32.dll: (error code %i) + err = GetLastError(); + goto done; + } + + PFNRELEASEPSEUDOCONSOLE pfnRelease = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"ReleasePseudoConsole"); + if (!pfnRelease) { + err = GetLastError(); + if (err == ERROR_PROC_NOT_FOUND) { + err = 0; + uv__release_pseudo_console(process->pty_handle); + } + else { + goto done; + } + } + else { + pfnRelease(process->pty_handle); + } + } + + /* Spawn succeeded. Beyond this point, failure is reported asynchronously. */ process->process_handle = info.hProcess; @@ -1154,6 +1538,7 @@ int uv_spawn(uv_loop_t* loop, uv__free(cwd); uv__free(env); uv__free(alloc_path); + uv__free(startupex.lpAttributeList); if (child_stdio_buffer != NULL) { /* Clean up child stdio handles. */ diff --git a/deps/uv/test/run-tests.c b/deps/uv/test/run-tests.c index 9c2574ebf8ab01..41ce5efcec5bf1 100644 --- a/deps/uv/test/run-tests.c +++ b/deps/uv/test/run-tests.c @@ -24,6 +24,7 @@ #include #ifdef _WIN32 +#include # include # define read _read #else @@ -247,6 +248,49 @@ static int maybe_run_test(int argc, char **argv) { spawn_stdin_stdout(); return 1; } + if (strcmp(argv[1], "spawn_helper10") == 0) { + char buffer[256]; + int cols, rows; + uv_tty_t tty; + notify_parent_process(); + uv_tty_init(uv_default_loop(), &tty, 1, 0); + + ASSERT_PTR_EQ(buffer, fgets(buffer, sizeof(buffer) - 1, stdin)); + buffer[sizeof(buffer) - 1] = '\0'; + + ASSERT_OK(uv_tty_get_winsize(&tty, &cols, &rows)); +#ifdef _WIN32 + printf("Is a TTY: %s\nWinSize: %ix%i\nRead: %s\n", _isatty(_fileno(stdin)) ? "true" : "false", cols, rows, buffer); +#else + printf("Is a TTY: %s\nWinSize: %ix%i\nRead: %s\n", isatty(STDIN_FILENO) ? "true" : "false", cols, rows, buffer); +#endif + + return 1; + } + if (strcmp(argv[1], "spawn_helper11") == 0) { + char inbuffer[256]; + char outbuffer[256]; + notify_parent_process(); + + ASSERT_PTR_EQ(inbuffer, fgets(inbuffer, sizeof(inbuffer) - 1, stdin)); + inbuffer[sizeof(inbuffer) - 1] = '\0'; + +#ifdef _WIN32 + snprintf(outbuffer, sizeof(outbuffer), "Is a TTY: %s\nRead: %s\n", _isatty(_fileno(stdin)) ? "true" : "false", inbuffer); + DWORD bytes; + WriteFile((HANDLE) _get_osfhandle(3), outbuffer, sizeof(outbuffer) - 1, &bytes, NULL); +#else + snprintf(outbuffer, sizeof(outbuffer), "Is a TTY: %s\nRead: %s\n", isatty(STDIN_FILENO) ? "true" : "false", inbuffer); + ssize_t r; + do + r = write(3, outbuffer, sizeof(outbuffer) - 1); + while (r == -1 && errno == EINTR); + fsync(3); +#endif + + return 1; + } + #ifndef _WIN32 if (strcmp(argv[1], "spawn_helper_setuid_setgid") == 0) { diff --git a/deps/uv/test/test-list.h b/deps/uv/test/test-list.h index b79a4ea74edf64..d505021220349e 100644 --- a/deps/uv/test/test-list.h +++ b/deps/uv/test/test-list.h @@ -349,6 +349,9 @@ TEST_DECLARE (spawn_quoted_path) TEST_DECLARE (spawn_tcp_server) TEST_DECLARE (spawn_exercise_sigchld_issue) TEST_DECLARE (spawn_relative_path) +TEST_DECLARE (spawn_pty_setup_succeeds) +TEST_DECLARE (spawn_pty_stdio_greater_than_3) +TEST_DECLARE (spawn_pty_resize) TEST_DECLARE (fs_poll) TEST_DECLARE (fs_poll_getpath) TEST_DECLARE (fs_poll_close_request) @@ -1038,6 +1041,9 @@ TASK_LIST_START TEST_ENTRY (spawn_tcp_server) TEST_ENTRY (spawn_exercise_sigchld_issue) TEST_ENTRY (spawn_relative_path) + TEST_ENTRY (spawn_pty_setup_succeeds) + TEST_ENTRY (spawn_pty_stdio_greater_than_3) + TEST_ENTRY (spawn_pty_resize) TEST_ENTRY (fs_poll) TEST_ENTRY (fs_poll_getpath) TEST_ENTRY (fs_poll_close_request) diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index eaaa8bbeac3d00..4125b3c481fdef 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -29,6 +29,7 @@ #include #ifdef _WIN32 +# include # include # include typedef BOOL (WINAPI *sCompareObjectHandles)(_In_ HANDLE, _In_ HANDLE); @@ -52,6 +53,7 @@ static char* args[5]; static int no_term_signal; static int timer_counter; static uv_tcp_t tcp_server; +uv_pipe_t pty_in; #define OUTPUT_SIZE 1024 static char output[OUTPUT_SIZE]; @@ -59,20 +61,30 @@ static int output_used; static void close_cb(uv_handle_t* handle) { - printf("close_cb\n"); close_cb_called++; } static void exit_cb(uv_process_t* process, int64_t exit_status, int term_signal) { - printf("exit_cb\n"); exit_cb_called++; ASSERT_EQ(1, exit_status); ASSERT_OK(term_signal); uv_close((uv_handle_t*) process, close_cb); } +static void pty_exit_cb(uv_process_t* process, + int64_t exit_status, + int term_signal) { + exit_cb_called++; + ASSERT_EQ(1, exit_status); + ASSERT_OK(term_signal); + uv_close((uv_handle_t*) process, close_cb); +#ifdef _WIN32 + uv_close((uv_handle_t*)&pty_in, close_cb); +#endif +} + static void fail_cb(uv_process_t* process, int64_t exit_status, @@ -86,7 +98,6 @@ static void kill_cb(uv_process_t* process, int term_signal) { int err; - printf("exit_cb\n"); exit_cb_called++; #ifdef _WIN32 ASSERT_EQ(1, exit_status); @@ -117,7 +128,6 @@ static void kill_cb(uv_process_t* process, static void detach_failure_cb(uv_process_t* process, int64_t exit_status, int term_signal) { - printf("detach_cb\n"); exit_cb_called++; } @@ -138,18 +148,36 @@ static void on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { } } +static void pty_on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { + if (nread > 0) { + output_used += nread; + } else if (nread < 0) { +#if defined(_WIN32) || defined(__APPLE__) + ASSERT_EQ(nread, UV_EOF); +#elif defined(__ANDROID__) + ASSERT(nread == UV_EIO || nread == UV_EOF); +#else + ASSERT_EQ(nread, UV_EIO); +#endif + uv_close((uv_handle_t*) tcp, close_cb); + } +} static void on_read_once(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { uv_read_stop(tcp); on_read(tcp, nread, buf); } - static void write_cb(uv_write_t* req, int status) { ASSERT_OK(status); uv_close((uv_handle_t*) req->handle, close_cb); } - +static void pty_write_cb(uv_write_t* req, int status) { + ASSERT_OK(status); +#ifndef _WIN32 + uv_close((uv_handle_t*) req->handle, close_cb); +#endif +} static void write_null_cb(uv_write_t* req, int status) { ASSERT_OK(status); @@ -183,6 +211,35 @@ static void timer_counter_cb(uv_timer_t* handle) { ++timer_counter; } +void filter_vt(char *t, char with_nl_and_sp) { + int wi = 0, len = strlen(t), csi = 0, osc = 0; + for (int ri = 0; ri < len; ri++) { + if (with_nl_and_sp && t[ri] == 13 && t[ri+1] == 10) { + ri++; + } + else if (with_nl_and_sp && t[ri] == ' ') { + // skip + } + else if (csi && 64 <= t[ri] && t[ri] <= 126) { + csi--; + } + else if (osc && t[ri] == 7) { + osc--; + } + else if (t[ri] == 27 && t[ri+1] == '[') { // ESC [ == CSI + ri++; + csi++; + } + else if (t[ri] == 27 && t[ri+1] == ']') { // ESC ] == OSC + ri++; + osc++; + } + else if (!csi && !osc) { + t[wi++] = t[ri]; + } + } + t[wi] = 0; +} TEST_IMPL(spawn_fails) { int r; @@ -2111,3 +2168,214 @@ TEST_IMPL(spawn_relative_path) { MAKE_VALGRIND_HAPPY(uv_default_loop()); return 0; } + +TEST_IMPL(spawn_pty_setup_succeeds) { + int r; + uv_pipe_t pty_out; + uv_write_t write_req; + uv_buf_t buf; + uv_stdio_container_t stdio[3]; +#ifdef _WIN32 + char buffer[] = "hello from parent\r\n"; +#else + char buffer[] = "hello from parent\n"; +#endif + + init_process_options("spawn_helper10", pty_exit_cb); + + uv_pipe_init(uv_default_loop(), &pty_in, 0); + uv_pipe_init(uv_default_loop(), &pty_out, 0); + + options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options.stdio = stdio; + options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options.stdio[2].flags = UV_IGNORE; + options.stdio_count = 3; + options.pty_cols = 72; + options.pty_rows = 24; + + r = uv_spawn(uv_default_loop(), &process, &options); + ASSERT_OK(r); + + // We don't want to write the trailing \0 + buf = uv_buf_init(buffer, sizeof(buffer) - 1); + + r = uv_write(&write_req, (uv_stream_t*) &pty_in, &buf, 1, pty_write_cb); + ASSERT_OK(r); + + r = uv_read_start((uv_stream_t*) &pty_out, on_alloc, pty_on_read); + ASSERT_OK(r); + + r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); + ASSERT_OK(r); + + ASSERT_EQ(1, exit_cb_called); + ASSERT_EQ(3, close_cb_called); /* Once for process twice for the pipes. */ + + /* We see the "hello from parent" string twice. The first is the terminal echo, + * the second is the reply from the child. + */ +#ifdef _WIN32 + /* We're trimming VT sequences that ConPTY sprinkles the output with. Also + * we're allowing trailing and leading stuff, because ConPTY sometimes + * adds '\r\n's at the start followed by a 'ESC[H' which moves the cursor + * back to the start, effectively a no-op. + */ + filter_vt(output, 0); + printf("Output: %s\n", output); + ASSERT_PTR_NE(strstr(output, "hello from parent\r\nIs a TTY: true\r\nWinSize: 72x24\r\nRead: hello from parent"), NULL); +#else + printf("Output: %s\n", output); + ASSERT_OK(strcmp("hello from parent\r\nIs a TTY: true\r\nWinSize: 72x24\r\nRead: hello from parent\r\n\r\n", output)); +#endif + + MAKE_VALGRIND_HAPPY(uv_default_loop()); + return 0; +} + +TEST_IMPL(spawn_pty_stdio_greater_than_3) { + int r; + uv_pipe_t pty_out, pipe4; + uv_write_t write_req; + uv_buf_t buf; + uv_stdio_container_t stdio[4]; +#ifdef _WIN32 + char buffer[] = "hello from parent\r\n"; +#else + char buffer[] = "hello from parent\n"; +#endif + + init_process_options("spawn_helper11", pty_exit_cb); + + uv_pipe_init(uv_default_loop(), &pty_in, 0); + uv_pipe_init(uv_default_loop(), &pty_out, 0); + uv_pipe_init(uv_default_loop(), &pipe4, 0); + + options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options.stdio = stdio; + options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options.stdio[2].flags = UV_IGNORE; + options.stdio[3].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options.stdio[3].data.stream = (uv_stream_t*) &pipe4; + options.stdio_count = 4; + options.pty_cols = 72; + options.pty_rows = 24; + + r = uv_spawn(uv_default_loop(), &process, &options); + ASSERT_OK(r); + + // We don't want to write the trailing \0 + buf = uv_buf_init(buffer, sizeof(buffer) - 1); + + r = uv_write(&write_req, (uv_stream_t*) &pty_in, &buf, 1, pty_write_cb); + ASSERT_OK(r); + + r = uv_read_start((uv_stream_t*) &pipe4, on_alloc, on_read); + ASSERT_OK(r); + + r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); + ASSERT_OK(r); + + ASSERT_EQ(1, exit_cb_called); + ASSERT_EQ(3, close_cb_called); /* Once for process twice for the pipes. */ + + printf("Output: %s\n", output); +#ifdef _WIN32 + ASSERT_OK(strcmp("Is a TTY: true\nRead: hello from parent\r\n\n", output)); +#else + ASSERT_OK(strcmp("Is a TTY: true\nRead: hello from parent\n\n", output)); +#endif + + MAKE_VALGRIND_HAPPY(uv_default_loop()); + return 0; +} + +TEST_IMPL(spawn_pty_resize) { + int r; + uv_pipe_t pty_out; + uv_write_t write_req; + uv_buf_t buf; + uv_stdio_container_t stdio[3]; +#ifdef _WIN32 + char buffer[] = "hello from parent\r\n"; +#else + char buffer[] = "hello from parent\n"; +#endif + + init_process_options("spawn_helper10", pty_exit_cb); + + uv_pipe_init(uv_default_loop(), &pty_in, 0); + uv_pipe_init(uv_default_loop(), &pty_out, 0); + + options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options.stdio = stdio; + options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options.stdio[2].flags = UV_IGNORE; + options.stdio_count = 3; + options.pty_cols = 72; + options.pty_rows = 24; + + r = uv_spawn(uv_default_loop(), &process, &options); + ASSERT_OK(r); + + // Workaround for https://github.com/microsoft/terminal/pull/10449. + // On older (Windows 10) ConPTY versions, the resize can be lost if it + // happens shortly after process startup. +#ifdef _WIN32 + Sleep(250); +#endif + + r = uv_pty_resize(&process, 10, 15); + ASSERT_OK(r); + + // We don't want to write the trailing \0 + buf = uv_buf_init(buffer, sizeof(buffer) - 1); + + r = uv_write(&write_req, (uv_stream_t*) &pty_in, &buf, 1, pty_write_cb); + ASSERT_OK(r); + + r = uv_read_start((uv_stream_t*) &pty_out, on_alloc, pty_on_read); + ASSERT_OK(r); + + r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); + ASSERT_OK(r); + + ASSERT_EQ(1, exit_cb_called); + ASSERT_EQ(3, close_cb_called); /* Once for process twice for the pipes. */ + + /* We see the "hello from parent" string twice. The first is the terminal echo, + * the second is the reply from the child. + */ +#ifdef _WIN32 + /* We're trimming VT sequences that ConPTY sprinkles the output with. Also + * we're allowing trailing and leading stuff, because ConPTY sometimes + * adds '\r\n's at the start followed by a 'ESC[H' which moves the cursor + * back to the start, effectively a no-op. + */ + + /* In addition older ConPTY versions (Windows 10) emit newlines when the + * terminal width is reached and put multiple spaces at the end of lines. + * so we'll filter out spaces and newlines as well. This abomination can be + * removed again, once Windows 10 support is dropped. + */ + filter_vt(output, 1); + printf("Output: %s\n", output); + ASSERT_PTR_NE(strstr(output, "hellofromparentIsaTTY:trueWinSize:10x15Read:hellofromparent"), NULL); + // ASSERT_PTR_NE(strstr(output, "hello from parent\r\nIs a TTY: true\r\nWinSize: 10x15\r\nRead: hello from parent"), NULL); +#else + printf("Output: %s\n", output); + ASSERT_OK(strcmp("hello from parent\r\nIs a TTY: true\r\nWinSize: 10x15\r\nRead: hello from parent\r\n\r\n", output)); +#endif + + MAKE_VALGRIND_HAPPY(uv_default_loop()); + return 0; +} From fc6f64f33148634ecb16702262dc03c2a8806d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Thu, 30 Oct 2025 22:07:11 +0100 Subject: [PATCH 02/13] deps: libuv: Address a few review comments --- deps/uv/src/unix/process.c | 66 ++++++++++++++++---------------------- deps/uv/test/test-spawn.c | 2 +- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 512decec26cf0a..7756a5f1f25c99 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -383,18 +383,19 @@ static void uv__process_child_init(const uv_process_options_t* options, } if (options->flags & UV_PROCESS_PTY) { - // Put ourself into a new session and process group, making us session - // and process group leader. - if (setsid() < 0) { - uv__write_errno(error_fd); - } + /* Put ourself into a new session and process group, making us session + * and process group leader. + */ + if (setsid() < 0) + uv__write_errno(error_fd); // Make our dear terminal the controlling terminal. if (ioctl(STDIN_FILENO, TIOCSCTTY) < 0) - uv__write_errno(error_fd); + uv__write_errno(error_fd); } - else if (options->flags & UV_PROCESS_DETACHED) + else if (options->flags & UV_PROCESS_DETACHED) { setsid(); + } if (options->cwd != NULL && chdir(options->cwd)) uv__write_errno(error_fd); @@ -987,16 +988,15 @@ int uv__pty_resize_fd(int pty_fd, unsigned short cols, unsigned short rows) { struct winsize winp; + memset(&winp, 0, sizeof(winp)); winp.ws_col = cols; winp.ws_row = rows; - winp.ws_xpixel = 0; - winp.ws_ypixel = 0; if (ioctl(pty_fd, TIOCSWINSZ, &winp) < 0) return UV__ERR(errno); return 0; } -int uv__spawn_make_pty(int *fd_pty, int *fd_tty, int cols, int rows) { +int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { int ret; int my_errno; @@ -1005,43 +1005,30 @@ int uv__spawn_make_pty(int *fd_pty, int *fd_tty, int cols, int rows) { return UV__ERR(errno); if (grantpt(*fd_pty) < 0) { - my_errno = UV__ERR(errno); - close(*fd_pty); - return my_errno; + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); } if (unlockpt(*fd_pty) < 0) { - my_errno = UV__ERR(errno); - close(*fd_pty); - return my_errno; + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); } - int path_tty_size = 40; - char *path_tty = uv__malloc(path_tty_size * sizeof(char)); + char path_tty[TTY_NAME_MAX + 1] = {0}; // Apple and linux both have ptsname_r. // Use TIOCGPTPEER. (see man ioctl_tty) Where is that available? - // There is no ptsname_r on OpenBSD. - while ((ret = ptsname_r(*fd_pty, path_tty, path_tty_size)) == ERANGE) { - path_tty_size *= 2; - path_tty = uv__realloc(path_tty, path_tty_size * sizeof(char)); - } - if (ret != 0) { - my_errno = UV__ERR(errno); - uv__free(path_tty); - close(*fd_pty); - return my_errno; + // TODO(patrickbkr): There is no ptsname_r on OpenBSD. + if (ptsname_r(*fd_pty, path_tty, TTY_NAME_MAX + 1) != 0) { + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); } *fd_tty = open(path_tty, O_RDWR | O_NOCTTY); if (*fd_tty < 0) { - my_errno = UV__ERR(errno); - uv__free(path_tty); - close(*fd_pty); - return my_errno; + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); } - uv__free(path_tty); - if ((my_errno = uv__pty_resize_fd(*fd_pty, cols, rows)) != 0) { close(*fd_pty); close(*fd_tty); @@ -1112,14 +1099,16 @@ int uv_spawn(uv_loop_t* loop, pipes[i][1] = -1; } - for (i = (options->flags & UV_PROCESS_PTY) ? 3 : 0; i < options->stdio_count; i++) { + for (i = (options->flags & UV_PROCESS_PTY) ? 3 : 0; + i < options->stdio_count; i++) { err = uv__process_init_stdio(options->stdio + i, pipes[i]); if (err) goto error; } if (options->flags & UV_PROCESS_PTY) { - if ((err = uv__spawn_make_pty(&process->pty_fd, &fd_tty, options->pty_cols, options->pty_rows)) != 0) + if ((err = uv__spawn_make_pty(&process->pty_fd, &fd_tty, options->pty_cols, + options->pty_rows)) != 0) goto error; pipes[0][1] = fd_tty; @@ -1138,7 +1127,8 @@ int uv_spawn(uv_loop_t* loop, #endif /* Spawn the child */ - exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, &pid, process->pty_fd); + exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, + &pid, process->pty_fd); #if 0 /* This runs into a nodejs issue (it expects initialized streams, even if the @@ -1175,7 +1165,7 @@ int uv_spawn(uv_loop_t* loop, } // We could special case this (do it as part of the below for loop) if it's ok for the pipe to be non-blocking. - // TODO: Validate this. + // TODO(patrickbkr): Validate this. if (options->flags & UV_PROCESS_PTY) { err = uv__close(fd_tty); diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index 4125b3c481fdef..ba524f3de3365e 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -211,7 +211,7 @@ static void timer_counter_cb(uv_timer_t* handle) { ++timer_counter; } -void filter_vt(char *t, char with_nl_and_sp) { +static void filter_vt(char* t, char with_nl_and_sp) { int wi = 0, len = strlen(t), csi = 0, osc = 0; for (int ri = 0; ri < len; ri++) { if (with_nl_and_sp && t[ri] == 13 && t[ri+1] == 10) { From 05f51c453ba989df4207d03a79cb373660441fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Tue, 4 Nov 2025 21:12:15 +0100 Subject: [PATCH 03/13] deps: libuv: Add a BSD implementation --- deps/uv/src/unix/process.c | 87 ++++++++++++++++++++++++-------------- deps/uv/test/test-spawn.c | 7 ++- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 7756a5f1f25c99..96ab89733debd5 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -38,6 +38,13 @@ #include #include +#if defined(__DragonFly__) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || \ + defined(__OpenBSD__) +#include +#endif + #if defined(__APPLE__) # include # include @@ -996,47 +1003,63 @@ int uv__pty_resize_fd(int pty_fd, return 0; } +/* The BSDs don't have ptsname_r. They do have ptsname, but that's not + * reentrant. The obvious alternative is openpty. + */ +#if defined(__DragonFly__) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || \ + defined(__OpenBSD__) +int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { + struct winsize winp; + memset(&winp, 0, sizeof(winp)); + winp.ws_col = cols; + winp.ws_row = rows; + if (openpty(fd_pty, fd_tty, 0, 0, &winp) < 0) { + return UV__ERR(errno); + } + return 0; +} +#else int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { - int ret; - int my_errno; + int ret; + int my_errno; - *fd_pty = posix_openpt(O_RDWR); - if (*fd_pty < 0) - return UV__ERR(errno); + *fd_pty = posix_openpt(O_RDWR); + if (*fd_pty < 0) + return UV__ERR(errno); - if (grantpt(*fd_pty) < 0) { - SAVE_ERRNO(close(*fd_pty)); - return UV__ERR(errno); - } + if (grantpt(*fd_pty) < 0) { + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); + } - if (unlockpt(*fd_pty) < 0) { - SAVE_ERRNO(close(*fd_pty)); - return UV__ERR(errno); - } + if (unlockpt(*fd_pty) < 0) { + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); + } - char path_tty[TTY_NAME_MAX + 1] = {0}; - // Apple and linux both have ptsname_r. - // Use TIOCGPTPEER. (see man ioctl_tty) Where is that available? - // TODO(patrickbkr): There is no ptsname_r on OpenBSD. - if (ptsname_r(*fd_pty, path_tty, TTY_NAME_MAX + 1) != 0) { - SAVE_ERRNO(close(*fd_pty)); - return UV__ERR(errno); - } + char path_tty[TTY_NAME_MAX + 1] = {0}; + if (ptsname_r(*fd_pty, path_tty, TTY_NAME_MAX + 1) != 0) { + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); + } - *fd_tty = open(path_tty, O_RDWR | O_NOCTTY); - if (*fd_tty < 0) { - SAVE_ERRNO(close(*fd_pty)); - return UV__ERR(errno); - } + *fd_tty = open(path_tty, O_RDWR | O_NOCTTY); + if (*fd_tty < 0) { + SAVE_ERRNO(close(*fd_pty)); + return UV__ERR(errno); + } - if ((my_errno = uv__pty_resize_fd(*fd_pty, cols, rows)) != 0) { - close(*fd_pty); - close(*fd_tty); - return my_errno; - } + if ((my_errno = uv__pty_resize_fd(*fd_pty, cols, rows)) != 0) { + close(*fd_pty); + close(*fd_tty); + return my_errno; + } - return 0; + return 0; } +#endif int uv_spawn(uv_loop_t* loop, uv_process_t* process, diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index ba524f3de3365e..c0dfc41f3de825 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -152,7 +152,12 @@ static void pty_on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { if (nread > 0) { output_used += nread; } else if (nread < 0) { -#if defined(_WIN32) || defined(__APPLE__) +#if defined(_WIN32) || \ + defined(__APPLE__) || \ + defined(__DragonFly__) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || \ + defined(__OpenBSD__) ASSERT_EQ(nread, UV_EOF); #elif defined(__ANDROID__) ASSERT(nread == UV_EIO || nread == UV_EOF); From c8606b443d8769b998a2591a2bdc86928a1717cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Tue, 4 Nov 2025 21:45:05 +0100 Subject: [PATCH 04/13] deps: libuv: Better error handling and docs re non-blocking PTY on unix --- deps/uv/src/unix/process.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 96ab89733debd5..6ac5b04ca7fd07 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -1187,16 +1187,24 @@ int uv_spawn(uv_loop_t* loop, uv__handle_start(process); } - // We could special case this (do it as part of the below for loop) if it's ok for the pipe to be non-blocking. - // TODO(patrickbkr): Validate this. + /* We do the PTY handles separately because STDIN and STDOUT share a handle. + * Doing it in the below loop would result in a double close. + * Also we are currently not setting the PTY pipes to non-blocking. That's + * not per se impossible, but that's for another time to investigate the + * ramifications a non-blocking PTY brings with it. */ if (options->flags & UV_PROCESS_PTY) { err = uv__close(fd_tty); - if ((err = uv_pipe_open((uv_pipe_t *)(options->stdio[0].data.stream), pipes[0][0])) != 0) - printf("uv_pipe_open 0 ret: %i\n", err); + for (i = 0; i < 2; i++) { + err = uv_pipe_open((uv_pipe_t *)(options->stdio[i].data.stream), pipes[i][0]); + if (err == 0) + continue; + + while (i--) + uv__process_close_stream(options->stdio + i); - if ((err = uv_pipe_open((uv_pipe_t *)(options->stdio[1].data.stream), pipes[1][0])) != 0) - printf("uv_pipe_open 1 ret: %i\n", err); + goto error; + } } for (i = (options->flags & UV_PROCESS_PTY) ? 3 : 0; i < options->stdio_count; i++) { From fc04e75087c2e890ee4ae9bb263389f71b356577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Tue, 4 Nov 2025 21:50:15 +0100 Subject: [PATCH 05/13] deps: libuv: ABI compat: Don't change the size of uv_process_t - Use the u.fd space conveniently added in UV_HANDLE_FIELDS. --- deps/uv/include/uv/unix.h | 1 - deps/uv/src/unix/process.c | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/deps/uv/include/uv/unix.h b/deps/uv/include/uv/unix.h index 43b069a332f3d4..c6ba419da98b39 100644 --- a/deps/uv/include/uv/unix.h +++ b/deps/uv/include/uv/unix.h @@ -357,7 +357,6 @@ typedef struct { #define UV_PROCESS_PRIVATE_FIELDS \ struct uv__queue queue; \ int status; \ - int pty_fd; #define UV_FS_PRIVATE_FIELDS \ const char *new_path; \ diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 6ac5b04ca7fd07..024d6cb4b476db 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -1103,7 +1103,7 @@ int uv_spawn(uv_loop_t* loop, uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS); uv__queue_init(&process->queue); process->status = 0; - process->pty_fd = -1; + process->u.fd = -1; stdio_count = options->stdio_count; if (stdio_count < 3) @@ -1130,15 +1130,15 @@ int uv_spawn(uv_loop_t* loop, } if (options->flags & UV_PROCESS_PTY) { - if ((err = uv__spawn_make_pty(&process->pty_fd, &fd_tty, options->pty_cols, + if ((err = uv__spawn_make_pty(&process->u.fd, &fd_tty, options->pty_cols, options->pty_rows)) != 0) goto error; pipes[0][1] = fd_tty; pipes[1][1] = fd_tty; pipes[2][1] = fd_tty; - pipes[0][0] = process->pty_fd; - if ((pipes[1][0] = dup(process->pty_fd)) < 0) { + pipes[0][0] = process->u.fd; + if ((pipes[1][0] = dup(process->u.fd)) < 0) { err = UV__ERR(errno); goto error; } @@ -1151,7 +1151,7 @@ int uv_spawn(uv_loop_t* loop, /* Spawn the child */ exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, - &pid, process->pty_fd); + &pid, process->u.fd); #if 0 /* This runs into a nodejs issue (it expects initialized streams, even if the @@ -1277,7 +1277,7 @@ void uv__process_close(uv_process_t* handle) { int uv_pty_resize(uv_process_t* process, unsigned short cols, unsigned short rows) { - if (process->pty_fd == -1) + if (process->u.fd == -1) return UV_EINVAL; - return uv__pty_resize_fd(process->pty_fd, cols, rows); + return uv__pty_resize_fd(process->u.fd, cols, rows); } From aa2a966f5b7ff3bd3046c0797fba246673a5c657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Tue, 4 Nov 2025 23:17:43 +0100 Subject: [PATCH 06/13] deps: libuv: ABI compat: Don't change the size of uv_process_options_t - Do so by introducing a `uv_spawn2` and `uv_process_options2_t`. --- deps/uv/docs/src/pipe.rst | 4 +- deps/uv/docs/src/process.rst | 86 +++++++++++++++++++++++------ deps/uv/include/uv.h | 58 +++++++++++++++++++- deps/uv/src/unix/process.c | 40 ++++++++++---- deps/uv/src/win/internal.h | 2 +- deps/uv/src/win/process-stdio.c | 2 +- deps/uv/src/win/process.c | 23 ++++++++ deps/uv/test/run-tests.c | 2 +- deps/uv/test/test-spawn.c | 96 +++++++++++++++++++-------------- 9 files changed, 242 insertions(+), 71 deletions(-) diff --git a/deps/uv/docs/src/pipe.rst b/deps/uv/docs/src/pipe.rst index bb77f90e3ce647..e01d101919a0f1 100644 --- a/deps/uv/docs/src/pipe.rst +++ b/deps/uv/docs/src/pipe.rst @@ -167,8 +167,8 @@ API Create a pair of connected pipe handles. Data may be written to `fds[1]` and read from `fds[0]`. - The resulting handles can be passed to `uv_pipe_open`, used with `uv_spawn`, - or for any other purpose. + The resulting handles can be passed to `uv_pipe_open`, used with + `uv_spawn` / `uv_spawn2` or for any other purpose. Valid values for `flags` are: diff --git a/deps/uv/docs/src/process.rst b/deps/uv/docs/src/process.rst index 33f844ec6d6b5d..48e14f5b22165a 100644 --- a/deps/uv/docs/src/process.rst +++ b/deps/uv/docs/src/process.rst @@ -17,6 +17,8 @@ Data types .. c:type:: uv_process_options_t + Deprecated, use `uv_spawn2` with `uv_process_options2_t` instead. + Options for spawning the process (passed to :c:func:`uv_spawn`. :: @@ -34,15 +36,37 @@ Data types uv_gid_t gid; } uv_process_options_t; +.. c:type:: uv_process_options2_t + + Options for spawning the process (passed to :c:func:`uv_spawn2`. + + :: + + typedef struct uv_process_options2_s { + int version; + uv_exit_cb exit_cb; + const char* file; + char** args; + char** env; + const char* cwd; + unsigned int flags; + int stdio_count; + uv_stdio_container_t* stdio; + unsigned int pty_cols; + unsigned int pty_rows; + uv_uid_t uid; + uv_gid_t gid; + } uv_process_options2_t; + .. c:type:: void (*uv_exit_cb)(uv_process_t*, int64_t exit_status, int term_signal) - Type definition for callback passed in :c:type:`uv_process_options_t` which + Type definition for callback passed in :c:type:`uv_process_options2_t` which will indicate the exit status and the signal that caused the process to terminate, if any. .. c:enum:: uv_process_flags - Flags to be set on the flags field of :c:type:`uv_process_options_t`. + Flags to be set on the flags field of :c:type:`uv_process_options2_t`. :: @@ -88,7 +112,7 @@ Data types UV_PROCESS_WINDOWS_HIDE_GUI = (1 << 6), /* * On Windows, if the path to the program to execute, specified in - * uv_process_options_t's file field, has a directory component, + * uv_process_options2_t's file field, has a directory component, * search for the exact file name before trying variants with * extensions like '.exe' or '.cmd'. */ @@ -186,36 +210,40 @@ Public members .. note:: The :c:type:`uv_handle_t` members also apply. -.. c:member:: uv_exit_cb uv_process_options_t.exit_cb +.. c:member:: uv_exit_cb uv_process_options2_t.version + + Version of the struct. Simply set it to UV_PROCESS_OPTIONS_VERSION. + +.. c:member:: uv_exit_cb uv_process_options2_t.exit_cb Callback called after the process exits. -.. c:member:: const char* uv_process_options_t.file +.. c:member:: const char* uv_process_options2_t.file Path pointing to the program to be executed. -.. c:member:: char** uv_process_options_t.args +.. c:member:: char** uv_process_options2_t.args Command line arguments. args[0] should be the path to the program. On Windows this uses `CreateProcess` which concatenates the arguments into a string this can cause some strange errors. See the ``UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS`` flag on :c:enum:`uv_process_flags`. -.. c:member:: char** uv_process_options_t.env +.. c:member:: char** uv_process_options2_t.env Environment for the new process. If NULL the parents environment is used. -.. c:member:: const char* uv_process_options_t.cwd +.. c:member:: const char* uv_process_options2_t.cwd Current working directory for the subprocess. -.. c:member:: unsigned int uv_process_options_t.flags +.. c:member:: unsigned int uv_process_options2_t.flags Various flags that control how :c:func:`uv_spawn` behaves. See :c:enum:`uv_process_flags`. -.. c:member:: int uv_process_options_t.stdio_count -.. c:member:: uv_stdio_container_t* uv_process_options_t.stdio +.. c:member:: int uv_process_options2_t.stdio_count +.. c:member:: uv_stdio_container_t* uv_process_options2_t.stdio The `stdio` field points to an array of :c:type:`uv_stdio_container_t` structs that describe the file descriptors that will be made available to @@ -226,8 +254,14 @@ Public members On Windows file descriptors greater than 2 are available to the child process only if the child processes uses the MSVCRT runtime. -.. c:member:: uv_uid_t uv_process_options_t.uid -.. c:member:: uv_gid_t uv_process_options_t.gid +.. c:member:: uv_gid_t uv_process_options2_t.pty_cols +.. c:member:: uv_uid_t uv_process_options2_t.pty_rows + + When installing a PTY via the `UV_PROCESS_PTY` flag, set the initial + pseudo terminal dimensions via these fields. + +.. c:member:: uv_uid_t uv_process_options2_t.uid +.. c:member:: uv_gid_t uv_process_options2_t.gid Libuv can change the child process' user/group id. This happens only when the appropriate bits are set in the flags fields. @@ -265,6 +299,9 @@ API .. c:function:: int uv_spawn(uv_loop_t* loop, uv_process_t* handle, const uv_process_options_t* options) + Deprecated. Use `uv_spawn2`. `uv_spawn2` behaves identical, but + takes a `uv_processs_options2_t` instead. + Initializes the process handle and starts the process. If the process is successfully spawned, this function will return 0. Otherwise, the negative error code corresponding to the reason it couldn't spawn is @@ -273,8 +310,7 @@ API Possible reasons for failing to spawn would include (but not be limited to) the file to execute not existing, not having permissions to use the setuid or setgid specified, or not having enough memory to allocate for the new - process. If the OS does not support PTYs and `UV_PROCESS_PTY` is passed - the error will be `UV_ENOTSUP`. + process. .. versionchanged:: 1.24.0 Added `UV_PROCESS_WINDOWS_HIDE_CONSOLE` and `UV_PROCESS_WINDOWS_HIDE_GUI` flags. @@ -282,7 +318,23 @@ API .. versionchanged:: 1.48.0 Added the `UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME` flag. - .. versionchanged:: 1.51.0 Added the `UV_PROCESS_PTY` flag. +.. c:function:: int uv_spawn2(uv_loop_t* loop, uv_process_t* handle, const uv_process_options2_t* options) + + Initializes the process handle and starts the process. If the process is + successfully spawned, this function will return 0. Otherwise, the + negative error code corresponding to the reason it couldn't spawn is + returned. + + Possible reasons for failing to spawn would include (but not be limited to) + the file to execute not existing, not having permissions to use the setuid or + setgid specified, or not having enough memory to allocate for the new + process. If the OS does not support PTYs and `UV_PROCESS_PTY` is passed + the error will be `UV_ENOTSUP`. + + .. versionadded:: 1.52.0 Identical to `uv_spawn` but takes a + `uv_process_options2_t` instead. With `uv_spawn2` it's + now possible to use a PTY via the `UV_PROCESS_PTY` flag. + .. c:function:: int uv_process_kill(uv_process_t* handle, int signum) @@ -306,6 +358,6 @@ API success, `UV_EINVAL` when the `process` is not PTY enabled, `UV_ENOTSUP` when the OS does not support PTYs. - .. versionadded:: 1.19.0 + .. versionadded:: 1.52.0 .. seealso:: The :c:type:`uv_handle_t` API functions also apply. diff --git a/deps/uv/include/uv.h b/deps/uv/include/uv.h index f25e2f9ee11dd9..18bb7b8490a005 100644 --- a/deps/uv/include/uv.h +++ b/deps/uv/include/uv.h @@ -1063,6 +1063,59 @@ typedef struct uv_stdio_container_s { } uv_stdio_container_t; typedef struct uv_process_options_s { + uv_exit_cb exit_cb; /* Called after the process exits. */ + const char* file; /* Path to program to execute. */ + /* + * Command line arguments. args[0] should be the path to the program. On + * Windows this uses CreateProcess which concatenates the arguments into a + * string this can cause some strange errors. See the note at + * windows_verbatim_arguments. + */ + char** args; + /* + * This will be set as the environ variable in the subprocess. If this is + * NULL then the parents environ will be used. + */ + char** env; + /* + * If non-null this represents a directory the subprocess should execute + * in. Stands for current working directory. + */ + const char* cwd; + /* + * Various flags that control how uv_spawn() behaves. See the definition of + * `enum uv_process_flags` below. + */ + unsigned int flags; + /* + * The `stdio` field points to an array of uv_stdio_container_t structs that + * describe the file descriptors that will be made available to the child + * process. The convention is that stdio[0] points to stdin, fd 1 is used for + * stdout, and fd 2 is stderr. + * + * Note that on windows file descriptors greater than 2 are available to the + * child process only if the child processes uses the MSVCRT runtime. + */ + int stdio_count; + uv_stdio_container_t* stdio; + /* + * Libuv can change the child process' user/group id. This happens only when + * the appropriate bits are set in the flags fields. This is not supported on + * windows; uv_spawn() will fail and set the error to UV_ENOTSUP. + */ + uv_uid_t uid; + uv_gid_t gid; +} uv_process_options_t; + +#define UV_PROCESS_OPTIONS_VERSION_V0 0 /* Same as original uv_process_options_t */ +#define UV_PROCESS_OPTIONS_VERSION_V1 1 /* With pty fields */ + +#define UV_PROCESS_OPTIONS_VERSION UV_PROCESS_OPTIONS_VERSION_V1 + +typedef struct uv_process_options2_s { + /* Struct version. Simply set it to UV_PROCESS_OPTIONS_VERSION. */ + int version; + uv_exit_cb exit_cb; /* Called after the process exits. */ const char* file; /* Path to program to execute. */ /* @@ -1111,7 +1164,7 @@ typedef struct uv_process_options_s { */ uv_uid_t uid; uv_gid_t gid; -} uv_process_options_t; +} uv_process_options2_t; /* * These are the flags that can be used for the uv_process_options.flags field. @@ -1191,6 +1244,9 @@ struct uv_process_s { UV_EXTERN int uv_spawn(uv_loop_t* loop, uv_process_t* handle, const uv_process_options_t* options); +UV_EXTERN int uv_spawn2(uv_loop_t* loop, + uv_process_t* handle, + const uv_process_options2_t* options); UV_EXTERN int uv_pty_resize(uv_process_t* process, unsigned short cols, unsigned short rows); diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 024d6cb4b476db..436c8507b0f529 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -293,7 +293,7 @@ static void uv__write_errno(int error_fd) { } -static void uv__process_child_init(const uv_process_options_t* options, +static void uv__process_child_init(const uv_process_options2_t* options, int stdio_count, int (*pipes)[2], int error_fd, @@ -494,7 +494,7 @@ static void uv__spawn_init_posix_spawn(void) { static int uv__spawn_set_posix_spawn_attrs( posix_spawnattr_t* attrs, const uv__posix_spawn_fncs_t* posix_spawn_fncs, - const uv_process_options_t* options) { + const uv_process_options2_t* options) { int err; unsigned int flags; sigset_t signal_set; @@ -566,7 +566,7 @@ static int uv__spawn_set_posix_spawn_attrs( static int uv__spawn_set_posix_spawn_file_actions( posix_spawn_file_actions_t* actions, const uv__posix_spawn_fncs_t* posix_spawn_fncs, - const uv_process_options_t* options, + const uv_process_options2_t* options, int stdio_count, int (*pipes)[2]) { int fd; @@ -700,7 +700,7 @@ char* uv__spawn_find_path_in_env(char** env) { } -static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options, +static int uv__spawn_resolve_and_spawn(const uv_process_options2_t* options, posix_spawnattr_t* attrs, posix_spawn_file_actions_t* actions, pid_t* pid) { @@ -801,7 +801,7 @@ static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options, static int uv__spawn_and_init_child_posix_spawn( - const uv_process_options_t* options, + const uv_process_options2_t* options, int stdio_count, int (*pipes)[2], pid_t* pid, @@ -841,7 +841,7 @@ static int uv__spawn_and_init_child_posix_spawn( } #endif -static int uv__spawn_and_init_child_fork(const uv_process_options_t* options, +static int uv__spawn_and_init_child_fork(const uv_process_options2_t* options, int stdio_count, int (*pipes)[2], int error_fd, @@ -885,7 +885,7 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options, static int uv__spawn_and_init_child( uv_loop_t* loop, - const uv_process_options_t* options, + const uv_process_options2_t* options, int stdio_count, int (*pipes)[2], pid_t* pid, @@ -1022,7 +1022,6 @@ int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { } #else int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { - int ret; int my_errno; *fd_pty = posix_openpt(O_RDWR); @@ -1064,6 +1063,27 @@ int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { int uv_spawn(uv_loop_t* loop, uv_process_t* process, const uv_process_options_t* options) { + uv_process_options2_t options2; + options2.version = UV_PROCESS_OPTIONS_VERSION_V0; + options2.exit_cb = options->exit_cb; + options2.file = options->file; + options2.args = options->args; + options2.env = options->env; + options2.cwd = options->cwd; + options2.flags = options->flags; + options2.stdio_count = options->stdio_count; + options2.stdio = options->stdio; + options2.pty_cols = 0; + options2.pty_rows = 0; + options2.uid = options->uid; + options2.gid = options->gid; + + return uv_spawn2(loop, process, &options2); +} + +int uv_spawn2(uv_loop_t* loop, + uv_process_t* process, + const uv_process_options2_t* options) { #if defined(__APPLE__) && (TARGET_OS_TV || TARGET_OS_WATCH) /* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */ return UV_ENOSYS; @@ -1078,13 +1098,15 @@ int uv_spawn(uv_loop_t* loop, int fd_tty; if (options->flags & UV_PROCESS_PTY) { + if (options->version < UV_PROCESS_OPTIONS_VERSION_V1) + return UV_EINVAL; if (options->stdio[0].flags != (UV_CREATE_PIPE | UV_READABLE_PIPE) || options->stdio[0].data.stream->type != UV_NAMED_PIPE || options->stdio[1].flags != (UV_CREATE_PIPE | UV_WRITABLE_PIPE) || options->stdio[1].data.stream->type != UV_NAMED_PIPE || options->stdio[2].flags != UV_IGNORE) return UV_EINVAL; - if (options->pty_rows == 0 | + if (options->pty_rows == 0 || options->pty_cols == 0) return UV_EINVAL; } diff --git a/deps/uv/src/win/internal.h b/deps/uv/src/win/internal.h index db488be77d9166..eeb755d669e500 100644 --- a/deps/uv/src/win/internal.h +++ b/deps/uv/src/win/internal.h @@ -276,7 +276,7 @@ int uv__random_winrandom(void* buf, size_t buflen); * Process stdio handles. */ int uv__stdio_create(uv_loop_t* loop, - const uv_process_options_t* options, + const uv_process_options2_t* options, BYTE** buffer_ptr); void uv__stdio_destroy(BYTE* buffer); void uv__stdio_noinherit(BYTE* buffer); diff --git a/deps/uv/src/win/process-stdio.c b/deps/uv/src/win/process-stdio.c index 2fbb07f1eb4f35..1cfb8b2286549b 100644 --- a/deps/uv/src/win/process-stdio.c +++ b/deps/uv/src/win/process-stdio.c @@ -167,7 +167,7 @@ int uv__create_nul_handle(HANDLE* handle_ptr, int uv__stdio_create(uv_loop_t* loop, - const uv_process_options_t* options, + const uv_process_options2_t* options, BYTE** buffer_ptr) { BYTE* buffer; int count, i; diff --git a/deps/uv/src/win/process.c b/deps/uv/src/win/process.c index 0ee34b23763d21..5820ec05414a43 100644 --- a/deps/uv/src/win/process.c +++ b/deps/uv/src/win/process.c @@ -1104,6 +1104,27 @@ HRESULT uv__release_pseudo_console(HPCON hPC) { int uv_spawn(uv_loop_t* loop, uv_process_t* process, const uv_process_options_t* options) { + uv_process_options2_t options2; + options2.version = UV_PROCESS_OPTIONS_VERSION_V0; + options2.exit_cb = options->exit_cb; + options2.file = options->file; + options2.args = options->args; + options2.env = options->env; + options2.cwd = options->cwd; + options2.flags = options->flags; + options2.stdio_count = options->stdio_count; + options2.stdio = options->stdio; + options2.pty_cols = 0; + options2.pty_rows = 0; + options2.uid = options->uid; + options2.gid = options->gid; + + return uv_spawn2(loop, process, &options2); +} + +int uv_spawn2(uv_loop_t* loop, + uv_process_t* process, + const uv_process_options2_t* options) { int i; int err = 0; @@ -1131,6 +1152,8 @@ int uv_spawn(uv_loop_t* loop, } if (options->flags & UV_PROCESS_PTY) { + if (options->version < UV_PROCESS_OPTIONS_VERSION_V1) + return UV_EINVAL; if (options->stdio[0].flags != (UV_CREATE_PIPE | UV_READABLE_PIPE) || options->stdio[0].data.stream->type != UV_NAMED_PIPE || options->stdio[1].flags != (UV_CREATE_PIPE | UV_WRITABLE_PIPE) || diff --git a/deps/uv/test/run-tests.c b/deps/uv/test/run-tests.c index 41ce5efcec5bf1..97b5bb3ba7a90a 100644 --- a/deps/uv/test/run-tests.c +++ b/deps/uv/test/run-tests.c @@ -268,7 +268,7 @@ static int maybe_run_test(int argc, char **argv) { return 1; } if (strcmp(argv[1], "spawn_helper11") == 0) { - char inbuffer[256]; + char inbuffer[200]; char outbuffer[256]; notify_parent_process(); diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index c0dfc41f3de825..dbe484fa96ccec 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -47,6 +47,7 @@ static int exit_cb_called; static uv_process_t process; static uv_timer_t timer; static uv_process_options_t options; +static uv_process_options2_t options2; static char exepath[1024]; static size_t exepath_size = 1024; static char* args[5]; @@ -205,6 +206,23 @@ static void init_process_options(char* test, uv_exit_cb exit_cb) { options.flags = 0; } +static void init_process_options2(char* test, uv_exit_cb exit_cb) { + /* Note spawn_helper1 defined in test/run-tests.c */ + int r = uv_exepath(exepath, &exepath_size); + ASSERT_OK(r); + exepath[exepath_size] = '\0'; + args[0] = exepath; + args[1] = test; + args[2] = NULL; + args[3] = NULL; + args[4] = NULL; + options2.version = UV_PROCESS_OPTIONS_VERSION; + options2.file = exepath; + options2.args = args; + options2.exit_cb = exit_cb; + options2.flags = 0; +} + static void timer_cb(uv_timer_t* handle) { uv_process_kill(&process, SIGTERM); @@ -2186,23 +2204,23 @@ TEST_IMPL(spawn_pty_setup_succeeds) { char buffer[] = "hello from parent\n"; #endif - init_process_options("spawn_helper10", pty_exit_cb); + init_process_options2("spawn_helper10", pty_exit_cb); uv_pipe_init(uv_default_loop(), &pty_in, 0); uv_pipe_init(uv_default_loop(), &pty_out, 0); - options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; - options.stdio = stdio; - options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; - options.stdio[0].data.stream = (uv_stream_t*) &pty_in; - options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - options.stdio[1].data.stream = (uv_stream_t*) &pty_out; - options.stdio[2].flags = UV_IGNORE; - options.stdio_count = 3; - options.pty_cols = 72; - options.pty_rows = 24; + options2.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options2.stdio = stdio; + options2.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options2.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options2.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options2.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options2.stdio[2].flags = UV_IGNORE; + options2.stdio_count = 3; + options2.pty_cols = 72; + options2.pty_rows = 24; - r = uv_spawn(uv_default_loop(), &process, &options); + r = uv_spawn2(uv_default_loop(), &process, &options2); ASSERT_OK(r); // We don't want to write the trailing \0 @@ -2253,26 +2271,26 @@ TEST_IMPL(spawn_pty_stdio_greater_than_3) { char buffer[] = "hello from parent\n"; #endif - init_process_options("spawn_helper11", pty_exit_cb); + init_process_options2("spawn_helper11", pty_exit_cb); uv_pipe_init(uv_default_loop(), &pty_in, 0); uv_pipe_init(uv_default_loop(), &pty_out, 0); uv_pipe_init(uv_default_loop(), &pipe4, 0); - options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; - options.stdio = stdio; - options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; - options.stdio[0].data.stream = (uv_stream_t*) &pty_in; - options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - options.stdio[1].data.stream = (uv_stream_t*) &pty_out; - options.stdio[2].flags = UV_IGNORE; - options.stdio[3].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - options.stdio[3].data.stream = (uv_stream_t*) &pipe4; - options.stdio_count = 4; - options.pty_cols = 72; - options.pty_rows = 24; + options2.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options2.stdio = stdio; + options2.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options2.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options2.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options2.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options2.stdio[2].flags = UV_IGNORE; + options2.stdio[3].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options2.stdio[3].data.stream = (uv_stream_t*) &pipe4; + options2.stdio_count = 4; + options2.pty_cols = 72; + options2.pty_rows = 24; - r = uv_spawn(uv_default_loop(), &process, &options); + r = uv_spawn2(uv_default_loop(), &process, &options2); ASSERT_OK(r); // We don't want to write the trailing \0 @@ -2313,23 +2331,23 @@ TEST_IMPL(spawn_pty_resize) { char buffer[] = "hello from parent\n"; #endif - init_process_options("spawn_helper10", pty_exit_cb); + init_process_options2("spawn_helper10", pty_exit_cb); uv_pipe_init(uv_default_loop(), &pty_in, 0); uv_pipe_init(uv_default_loop(), &pty_out, 0); - options.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; - options.stdio = stdio; - options.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; - options.stdio[0].data.stream = (uv_stream_t*) &pty_in; - options.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - options.stdio[1].data.stream = (uv_stream_t*) &pty_out; - options.stdio[2].flags = UV_IGNORE; - options.stdio_count = 3; - options.pty_cols = 72; - options.pty_rows = 24; - - r = uv_spawn(uv_default_loop(), &process, &options); + options2.flags |= UV_PROCESS_PTY | UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + options2.stdio = stdio; + options2.stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + options2.stdio[0].data.stream = (uv_stream_t*) &pty_in; + options2.stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + options2.stdio[1].data.stream = (uv_stream_t*) &pty_out; + options2.stdio[2].flags = UV_IGNORE; + options2.stdio_count = 3; + options2.pty_cols = 72; + options2.pty_rows = 24; + + r = uv_spawn2(uv_default_loop(), &process, &options2); ASSERT_OK(r); // Workaround for https://github.com/microsoft/terminal/pull/10449. From b86cbd92534024097cae345171d0864886d49494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Wed, 5 Nov 2025 21:48:51 +0100 Subject: [PATCH 07/13] deps: libuv: Fix MacOS build - The TTY_NAME_MAX constant isn't available on MacOS. Use - `sysconf(_SC_TTY_NAME_MAX)` instead. --- deps/uv/src/unix/process.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 436c8507b0f529..5d8c4e7f63f487 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -1038,18 +1038,23 @@ int uv__spawn_make_pty(int* fd_pty, int* fd_tty, int cols, int rows) { return UV__ERR(errno); } - char path_tty[TTY_NAME_MAX + 1] = {0}; - if (ptsname_r(*fd_pty, path_tty, TTY_NAME_MAX + 1) != 0) { + int path_tty_size = sysconf(_SC_TTY_NAME_MAX) + 1; + char *path_tty = uv__malloc(path_tty_size * sizeof(char)); + if (ptsname_r(*fd_pty, path_tty, path_tty_size) != 0) { SAVE_ERRNO(close(*fd_pty)); return UV__ERR(errno); } *fd_tty = open(path_tty, O_RDWR | O_NOCTTY); if (*fd_tty < 0) { - SAVE_ERRNO(close(*fd_pty)); - return UV__ERR(errno); + my_errno = UV__ERR(errno); + close(*fd_pty); + uv__free(path_tty); + return my_errno; } + uv__free(path_tty); + if ((my_errno = uv__pty_resize_fd(*fd_pty, cols, rows)) != 0) { close(*fd_pty); close(*fd_tty); From 458a68dcd61f11f631141067f9f5a040c64be695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Wed, 5 Nov 2025 21:49:21 +0100 Subject: [PATCH 08/13] deps: libuv: Fix a "unused function" build warning --- deps/uv/test/test-spawn.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index dbe484fa96ccec..386f91f943e246 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -234,6 +234,7 @@ static void timer_counter_cb(uv_timer_t* handle) { ++timer_counter; } +#ifdef _WIN32 static void filter_vt(char* t, char with_nl_and_sp) { int wi = 0, len = strlen(t), csi = 0, osc = 0; for (int ri = 0; ri < len; ri++) { @@ -263,6 +264,7 @@ static void filter_vt(char* t, char with_nl_and_sp) { } t[wi] = 0; } +#endif TEST_IMPL(spawn_fails) { int r; From 371acfe2b3b4023a95ce400cc2ccb7e0eec79542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Sat, 3 Jan 2026 23:44:50 +0100 Subject: [PATCH 09/13] deps: libuv: Persist and lazily initialize `ResizePseudoConsole` func - Both GetProcAddress as well as LoadLibrary are fairly expensive. So cache - the result. This is racy, but it doesn't matter much, on a collision we'll - do a bit of throw away work, but the assignment of `pfnResize` is atomic. - So nothing bad will happen. --- deps/uv/src/win/process.c | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/deps/uv/src/win/process.c b/deps/uv/src/win/process.c index 5820ec05414a43..21fc5bd83a2131 100644 --- a/deps/uv/src/win/process.c +++ b/deps/uv/src/win/process.c @@ -935,20 +935,25 @@ void uv__process_endgame(uv_loop_t* loop, uv_process_t* handle) { int uv_pty_resize(uv_process_t* process, unsigned short cols, unsigned short rows) { + static PFNRESIZEPSEUDOCONSOLE pfnResize = NULL; + if (process->pty_handle == NULL) return UV_EINVAL; - HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); - // Error loading kernel32.dll: (error code %i) - if (!hLibrary) - return uv_translate_sys_error(GetLastError()); - PFNRESIZEPSEUDOCONSOLE pfnResize = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"ResizePseudoConsole"); - if (!pfnResize) { - int err = GetLastError(); - if (err == ERROR_PROC_NOT_FOUND) - return UV_ENOTSUP; - else - return uv_translate_sys_error(err); + if (pfnResize == NULL) { + HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); + // Error loading kernel32.dll: (error code %i) + if (!hLibrary) + return uv_translate_sys_error(GetLastError()); + + pfnResize = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,"ResizePseudoConsole"); + if (!pfnResize) { + int err = GetLastError(); + if (err == ERROR_PROC_NOT_FOUND) + return UV_ENOTSUP; + else + return uv_translate_sys_error(err); + } } COORD size = {cols, rows}; From 4b03a58d97fa3f4e6587b232db4302568ec39feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Sat, 3 Jan 2026 23:45:35 +0100 Subject: [PATCH 10/13] deps: libuv: Enlarge the PTY out read buffer - Should be at least 128KiB according to lhecker++. --- deps/uv/src/win/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/uv/src/win/process.c b/deps/uv/src/win/process.c index 21fc5bd83a2131..58d60200365997 100644 --- a/deps/uv/src/win/process.c +++ b/deps/uv/src/win/process.c @@ -1037,7 +1037,7 @@ typedef struct { HANDLE pty_out_read; uv_pipe_t out_write; } uv__pty_out_data; -const int out_read_buf_len = 512; +const int out_read_buf_len = 128 * 1024; void pty_stdout_read_cb(uv_work_t *req) { int err = 0; From 468f983a0338ca1c841f7048c45462e5ae40bade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Sat, 3 Jan 2026 23:48:39 +0100 Subject: [PATCH 11/13] deps: libuv: Speed up `uv_pty_resize` even more - kernel32.dll will always be loaded, so we can just retrieve its handle - instead of loading it. --- deps/uv/src/win/process.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deps/uv/src/win/process.c b/deps/uv/src/win/process.c index 58d60200365997..bf6eecb520a64a 100644 --- a/deps/uv/src/win/process.c +++ b/deps/uv/src/win/process.c @@ -941,8 +941,7 @@ int uv_pty_resize(uv_process_t* process, return UV_EINVAL; if (pfnResize == NULL) { - HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0); - // Error loading kernel32.dll: (error code %i) + HANDLE hLibrary = GetModuleHandleW(L"kernel32.dll"); if (!hLibrary) return uv_translate_sys_error(GetLastError()); From 158781b537e1dd8e3baadff21254b9bad9dbfa53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6ker?= Date: Wed, 7 Jan 2026 12:01:56 +0100 Subject: [PATCH 12/13] deps: libuv: Make PTY tests more robust - Let's just punt on the EOF vs EIO issue. --- deps/uv/test/test-spawn.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c index 386f91f943e246..7554d0caeca575 100644 --- a/deps/uv/test/test-spawn.c +++ b/deps/uv/test/test-spawn.c @@ -153,18 +153,10 @@ static void pty_on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { if (nread > 0) { output_used += nread; } else if (nread < 0) { -#if defined(_WIN32) || \ - defined(__APPLE__) || \ - defined(__DragonFly__) || \ - defined(__FreeBSD__) || \ - defined(__NetBSD__) || \ - defined(__OpenBSD__) - ASSERT_EQ(nread, UV_EOF); -#elif defined(__ANDROID__) +/* Depending on the platform we either get UV_EOF or UV_EIO as the stream + * close indicator. Windows and BSDs do EOF, Linux does EIO, Android does both. + */ ASSERT(nread == UV_EIO || nread == UV_EOF); -#else - ASSERT_EQ(nread, UV_EIO); -#endif uv_close((uv_handle_t*) tcp, close_cb); } } From 0dc26a149ce59ac77a948e0d5d9aad72825641ff Mon Sep 17 00:00:00 2001 From: Yonggang Luo Date: Mon, 13 Jul 2026 20:24:55 +0800 Subject: [PATCH 13/13] child_process: add PTY support for spawn and spawnSync - Expose options.pty and stdio 'pty' so children get a real TTY via libuv ConPTY/openpty, with combined stdout and null stderr. - Wire process_wrap and spawn_sync to uv_spawn2, pty cols/rows, and uv_pty_resize; skip early PTY stdin shutdown that ConPTY treats as CTRL_C. - Reject incompatible options (windowsHide; detached on Windows), replace TERM when pty.name is set, and throw from resize on invalid state. - Add docs, contributing plan, and parallel coverage for remap, TERM, shell, validation, and resize. --- doc/api/child_process.md | 82 ++++- lib/child_process.js | 36 ++- lib/internal/child_process.js | 237 ++++++++++++++- src/env_properties.h | 3 + src/process_wrap.cc | 95 +++++- src/spawn_sync.cc | 66 ++++- src/spawn_sync.h | 2 +- test/parallel/test-child-process-spawn-pty.js | 279 ++++++++++++++++++ 8 files changed, 768 insertions(+), 32 deletions(-) create mode 100644 test/parallel/test-child-process-spawn-pty.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index e90759b16d3fa8..44b0bdd55dde09 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -684,7 +684,8 @@ changes: [`options.stdio`][`stdio`]). * `detached` {boolean} Prepare child process to run independently of its parent process. Specific behavior depends on the platform (see - [`options.detached`][]). + [`options.detached`][]). Cannot be used together with [`options.pty`][] + on Windows. * `uid` {number} Sets the user identity of the process (see setuid(2)). * `gid` {number} Sets the group identity of the process (see setgid(2)). * `serialization` {string} Specify the kind of serialization used for sending @@ -699,6 +700,9 @@ changes: when `shell` is specified and is CMD. **Default:** `false`. * `windowsHide` {boolean} Hide the subprocess console window that would normally be created on Windows systems. **Default:** `false`. + Cannot be used together with [`options.pty`][]. + * `pty` {boolean|Object} Spawn the child with a pseudo-terminal. + See [`options.pty`][]. **Default:** `false`. * `signal` {AbortSignal} allows aborting the child process using an AbortSignal. * `timeout` {number} In milliseconds the maximum amount of time the process @@ -907,7 +911,8 @@ added: v0.7.10 On Windows, setting `options.detached` to `true` makes it possible for the child process to continue running after the parent exits. The child process will have its own console window. Once enabled for a child process, -it cannot be disabled. +it cannot be disabled. `options.detached` cannot be used together with +[`options.pty`][] on Windows. On non-Windows platforms, if `options.detached` is set to `true`, the child process will be made the leader of a new process group and session. Child @@ -983,6 +988,32 @@ const subprocess = spawn('prg', [], { subprocess.unref(); ``` +#### `options.pty` + + + +* `pty` {boolean|Object} If `true` or an object, spawn the child with a + pseudo-terminal so that `stdin`/`stdout`/`stderr` in the child are TTYs. + `false` disables PTY mode. When `true`, uses `process.stdout.columns`/`rows` + when available, otherwise `80x24`. When an object: + * `cols` {number} Terminal width. Must be an integer `>= 1`. + * `rows` {number} Terminal height. Must be an integer `>= 1`. + * `name` {string} Optional value for the child's `TERM` environment + variable. + +Enabling PTY remaps stdio fds 0-2 to `['pipe', 'pty', 'pty']`: the parent +writes to stdin, reads combined stdout/stderr from the PTY out stream, and +`subprocess.stderr` is `null`. Fd 0 must be `'pipe'` (or defaulted); an +explicit `'pty'` on stdin throws `ERR_INVALID_ARG_VALUE`. `'ipc'` stdio +entries are not supported together with PTY. `options.windowsHide` cannot be +used with PTY. On Windows, `options.detached` cannot be used with PTY (ConPTY +does not deliver output to the parent). When `options.pty.name` is set, it +replaces any existing `TERM` in the child environment. On Windows (ConPTY), +output may include terminal VT/OSC sequences. +See also [`options.stdio`][`stdio`]. + #### `options.stdio` + +* `cols` {number} Terminal width. Must be an integer `>= 1`. +* `rows` {number} Terminal height. Must be an integer `>= 1`. +* Returns: {number} `0` on success, or a libuv error code. + +Resizes the pseudo-terminal for a child spawned with [`options.pty`][]. Throws +`ERR_OUT_OF_RANGE` if `cols` or `rows` is less than `1`. Throws +`ERR_INVALID_STATE` if the child was not spawned with PTY mode, or if the +process handle is already closed. + ### `subprocess.kill([signal])`