This commit is contained in:
Patrick Böker 2026-03-18 15:53:27 +03:00 committed by GitHub
commit 187ebc9a4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1187 additions and 93 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

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

View File

@ -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,11 +112,20 @@ 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'.
*/
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
@ -177,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
@ -217,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.
@ -256,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,6 +319,24 @@ API
.. versionchanged:: 1.48.0 Added the
`UV_PROCESS_WINDOWS_FILE_PATH_EXACT_NAME` 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)
Sends the specified signal to the given process handle. Check the documentation
@ -289,4 +353,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.52.0
.. seealso:: The :c:type:`uv_handle_t` API functions also apply.

View File

@ -1107,6 +1107,65 @@ typedef struct uv_process_options_s {
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. */
/*
* 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;
/*
* 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
* windows; uv_spawn() will fail and set the error to UV_ENOTSUP.
*/
uv_uid_t uid;
uv_gid_t gid;
} uv_process_options2_t;
/*
* These are the flags that can be used for the uv_process_options.flags field.
*/
@ -1160,7 +1219,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 +1244,12 @@ 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);
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

@ -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,24 @@
#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(__DragonFly__) || \
defined(__FreeBSD__) || \
defined(__NetBSD__) || \
defined(__OpenBSD__)
#include <util.h>
#endif
#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>
@ -284,10 +293,14 @@ 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) {
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 +327,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 +389,21 @@ 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);
@ -469,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;
@ -541,7 +566,7 @@ error:
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;
@ -675,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) {
@ -776,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,
@ -816,11 +841,12 @@ error:
}
#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,
pid_t* pid) {
pid_t* pid,
int close_fd) {
sigset_t signewset;
sigset_t sigoldset;
@ -842,7 +868,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();
}
@ -859,10 +885,11 @@ 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) {
pid_t* pid,
int close_fd) {
int signal_pipe[2] = { -1, -1 };
int status;
int err;
@ -870,33 +897,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 +954,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,9 +991,104 @@ 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;
memset(&winp, 0, sizeof(winp));
winp.ws_col = cols;
winp.ws_row = rows;
if (ioctl(pty_fd, TIOCSWINSZ, &winp) < 0)
return UV__ERR(errno);
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 my_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 (unlockpt(*fd_pty) < 0) {
SAVE_ERRNO(close(*fd_pty));
return UV__ERR(errno);
}
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) {
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);
return my_errno;
}
return 0;
}
#endif
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;
@ -977,6 +1100,21 @@ 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->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 ||
options->pty_cols == 0)
return UV_EINVAL;
}
assert(options->file != NULL);
assert(!(options->flags & ~(UV_PROCESS_DETACHED |
@ -986,11 +1124,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->u.fd = -1;
stdio_count = options->stdio_count;
if (stdio_count < 3)
@ -1009,18 +1149,36 @@ 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->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->u.fd;
if ((pipes[1][0] = dup(process->u.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->u.fd);
#if 0
/* This runs into a nodejs issue (it expects initialized streams, even if the
@ -1056,7 +1214,27 @@ int uv_spawn(uv_loop_t* loop,
uv__handle_start(process);
}
for (i = 0; i < options->stdio_count; i++) {
/* 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);
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);
goto error;
}
}
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 +1300,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->u.fd == -1)
return UV_EINVAL;
return uv__pty_resize_fd(process->u.fd, cols, rows);
}

View File

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

View File

@ -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;
@ -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,212 @@ 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) {
static PFNRESIZEPSEUDOCONSOLE pfnResize = NULL;
if (process->pty_handle == NULL)
return UV_EINVAL;
if (pfnResize == NULL) {
HANDLE hLibrary = GetModuleHandleW(L"kernel32.dll");
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};
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 = 128 * 1024;
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) {
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;
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 +1155,23 @@ int uv_spawn(uv_loop_t* loop,
return UV_EINVAL;
}
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->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 +1180,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 +1261,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 +1271,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 +1404,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 +1424,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 +1495,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 +1565,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[200];
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

@ -351,6 +351,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)
@ -1043,6 +1046,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);
@ -46,12 +47,14 @@ 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];
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 +62,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 +99,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 +129,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 +149,33 @@ 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) {
/* 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);
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);
@ -172,6 +198,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);
@ -183,6 +226,37 @@ 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++) {
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;
}
#endif
TEST_IMPL(spawn_fails) {
int r;
@ -2111,3 +2185,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_options2("spawn_helper10", pty_exit_cb);
uv_pipe_init(uv_default_loop(), &pty_in, 0);
uv_pipe_init(uv_default_loop(), &pty_out, 0);
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);
// 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_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);
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_spawn2(uv_default_loop(), &process, &options2);
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_options2("spawn_helper10", pty_exit_cb);
uv_pipe_init(uv_default_loop(), &pty_in, 0);
uv_pipe_init(uv_default_loop(), &pty_out, 0);
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.
// 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;
}