Implement PTY support

This commit is contained in:
Patrick Böker 2025-02-22 21:47:50 +01:00
parent 91ae02a63d
commit aee382337c
11 changed files with 965 additions and 70 deletions

View File

@ -56,6 +56,12 @@ jobs:
sanitizers-macos:
runs-on: macos-14
env:
# The sanitizer interferes with malloc memory preallocation causing a
# harmless warning, which interferes with tests that assert program
# output. This env var gets rid of the warning.
# See https://stackoverflow.com/a/70209891
MallocNanoZone: 0
strategy:
matrix:
config:

View File

@ -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.

View File

@ -1100,6 +1100,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
@ -1162,7 +1168,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)
};
/*
@ -1178,6 +1193,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*);

View File

@ -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; \

View File

@ -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 \

View File

@ -31,15 +31,17 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <termios.h>
#if defined(__APPLE__)
# include <spawn.h>
# include <paths.h>
# include <sys/kauth.h>
# include <sys/types.h>
# include <sys/sysctl.h>
# include <dlfcn.h>
# include <crt_externs.h>
@ -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);
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;
/* 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);
}

View File

@ -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];

View File

@ -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;
startup.cbReserved2 = uv__stdio_size(child_stdio_buffer);
startup.lpReserved2 = (BYTE*) child_stdio_buffer;
err = uv__stdio_create(loop, options, &child_stdio_buffer);
if (err)
goto done;
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);
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);
/* Now setup the PTY object
*/
COORD size = {options->pty_cols, options->pty_rows};
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. */

View File

@ -24,6 +24,7 @@
#include <string.h>
#ifdef _WIN32
#include <windows.h>
# include <io.h>
# 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) {

View File

@ -346,6 +346,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)
@ -1031,6 +1034,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)

View File

@ -29,6 +29,7 @@
#include <string.h>
#ifdef _WIN32
# include <synchapi.h>
# include <shellapi.h>
# include <wchar.h>
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;
}