Add *pty API (#3221)
* Initial function signatur and struct for `RzPty` API
* Change type annotation to borrow
* Implement `rz_subprocess_login_tty`
* Add fork mode enum and move pty struct; restructure ifdef guards
* Add rough implementation of forkpty
* Add `rz_sys` versions for `forkpty` family of functions
* Add forkpty code; Make changes according to review
* Implement forkpty properly and add unit test
* Add `test_interactive_pty`
* Add documentation
* Remove const in function argument
* Apparently, MacOS as non-const arguments for `openpty` and `forkpty`
* Add more tests for subprocess forkin using forkpty
* `test_interactive_pipe_pty`: Create pipes between the slave's PTY
and master and use them
* `test_interactive_custom_pty`: Use a custom PTY for the slave
* Add PTY functions exist checks for unit tests
* Move terminal param code inside the forkpty branch
* Add comment about why we need to check `n_bytes = 0` in `subprocess_wait`
* Remove forkpty with a specified `RzPTY`
* Also, no piping done in forkpty
* Add checks to disallow using `forkpty` with piping
* Make changes according to review
* No need to check proc file descriptors for -1 in `forkpty` mode, since
they *will* be -1
* No need to check `n_bytes == 0` in for `std{out,err}_pty`
* Extract pipe initialization code in `init_pipes` function
* Allow using custom PTY for `rz_subprocess_forkpty`
* Remove const qualifiers for termios and winsize arguments
* This makes the build pass on MacOS, which does accepts non-const
arguments
* Change `pid_t` to int so that it compiles on Windows
* Add empty implementation for PTY API for Windows
* Update function signature for `rz_subprocess_start_opt` for Windows
* Re-add `#define` for `pid_t` in `librz/socket/run.c`
* Move functions to remove duplicate definitions
* Allow specifying piping options even in `forkpty` mode
* Only expose the master fd for the streams which have pipe create
as their piping option
* Add comment about not using `master_fd` directly
* Fix types in definition for Windows
* Free the allocated `RzPTY` object
* Add const qualifiers for `RzPTY` argument
* Assign `fork_mode` and `pty` in all instances of subprocess opt start
* for good measure
* Compare master fd with -1, not 0
* Remove stray file
* Add second comment about the terminal attribute
* Overhaul the whole PTY implementation
* Now the caller can either use a pipe or PTY to send and receive
data from any of the streams
* Extra option `make_raw` to use the slave PTY in raw mode
* Add documentation for all the new functions
* Fix UAF bug
* Add error handling for `tcgetattr`
* Move the term param setting code
* Add documentation for the subprocess API and sys API
* Format according to clang-format
This commit is contained in:
parent
276306b587
commit
d20f68d7ab
5 changed files with 552 additions and 68 deletions
|
|
@ -16,7 +16,9 @@ typedef enum rz_subprocess_pipe_create_t {
|
|||
RZ_SUBPROCESS_PIPE_NONE,
|
||||
///< A new pipe should be created. It can be used for stdin, stdout and stderr.
|
||||
RZ_SUBPROCESS_PIPE_CREATE,
|
||||
///< Re-use the same pipe as stdout. It can be used for stderr only.
|
||||
///< A new PTY should be created. It can be used for stdin, stdout and stderr.
|
||||
RZ_SUBPROCESS_PIPE_PTY,
|
||||
///< Re-use the same pipe/PTY as stdout. It can be used for stderr only.
|
||||
RZ_SUBPROCESS_PIPE_STDOUT,
|
||||
} RzSubprocessPipeCreate;
|
||||
|
||||
|
|
@ -33,6 +35,11 @@ typedef enum rz_process_wait_reason_t {
|
|||
RZ_SUBPROCESS_BYTESREAD,
|
||||
} RzSubprocessWaitReason;
|
||||
|
||||
typedef enum rz_subprocess_fork_mode_t {
|
||||
RZ_SUBPROCESS_FORK,
|
||||
RZ_SUBPROCESS_FORKPTY,
|
||||
} RzSubprocessForkMode;
|
||||
|
||||
/**
|
||||
* Provide results from running a sub-process, like output, return value, etc.
|
||||
*/
|
||||
|
|
@ -51,6 +58,12 @@ typedef struct rz_process_output_t {
|
|||
bool timeout;
|
||||
} RzSubprocessOutput;
|
||||
|
||||
typedef struct rz_pty_t {
|
||||
int master_fd;
|
||||
int slave_fd;
|
||||
char *name;
|
||||
} RzPty;
|
||||
|
||||
/**
|
||||
* Specify how the new subprocess should be created.
|
||||
*/
|
||||
|
|
@ -73,6 +86,14 @@ typedef struct rz_subprocess_opt_t {
|
|||
RzSubprocessPipeCreate stdout_pipe;
|
||||
///< Specify how to deal with subprocess stderr
|
||||
RzSubprocessPipeCreate stderr_pipe;
|
||||
|
||||
/* Both the following fields only matter when using PTY in the pipe options */
|
||||
///< Provide the PTY to use (if NULL, then a new one will be created, if required)
|
||||
///< No need to close the PTY once it has been used, you can free it directly
|
||||
RzPty *pty;
|
||||
///< Use raw mode for the created PTY (disable echo, control characters, etc.)
|
||||
///< If you don't know what to put here, put true here.
|
||||
bool make_raw;
|
||||
} RzSubprocessOpt;
|
||||
|
||||
typedef struct rz_subprocess_t RzSubprocess;
|
||||
|
|
@ -82,7 +103,7 @@ RZ_API void rz_subprocess_fini(void);
|
|||
RZ_API RzSubprocess *rz_subprocess_start(
|
||||
const char *file, const char *args[], size_t args_size,
|
||||
const char *envvars[], const char *envvals[], size_t env_size);
|
||||
RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt);
|
||||
RZ_API RZ_OWN RzSubprocess *rz_subprocess_start_opt(RZ_NONNULL const RzSubprocessOpt *opt);
|
||||
RZ_API void rz_subprocess_free(RzSubprocess *proc);
|
||||
RZ_API RzSubprocessWaitReason rz_subprocess_wait(RzSubprocess *proc, ut64 timeout_ms);
|
||||
RZ_API void rz_subprocess_kill(RzSubprocess *proc);
|
||||
|
|
@ -91,8 +112,13 @@ RZ_API ut8 *rz_subprocess_out(RzSubprocess *proc, int *length);
|
|||
RZ_API ut8 *rz_subprocess_err(RzSubprocess *proc, int *length);
|
||||
RZ_API ssize_t rz_subprocess_stdin_write(RzSubprocess *proc, const ut8 *buf, size_t buf_size);
|
||||
RZ_API RzStrBuf *rz_subprocess_stdout_read(RzSubprocess *proc, size_t n, ut64 timeout_ms);
|
||||
RZ_API RzStrBuf *rz_subprocess_stdout_readline(RzSubprocess *proc, ut64 timeout_ms);
|
||||
RZ_API RZ_BORROW RzStrBuf *rz_subprocess_stdout_readline(RzSubprocess *proc, ut64 timeout_ms);
|
||||
RZ_API RzSubprocessOutput *rz_subprocess_drain(RzSubprocess *proc);
|
||||
RZ_API void rz_subprocess_output_free(RzSubprocessOutput *out);
|
||||
|
||||
#endif
|
||||
RZ_API RZ_OWN RzPty *rz_subprocess_openpty(RZ_BORROW RZ_NULLABLE char *slave_name, RZ_NULLABLE void /* const struct termios */ *term_params, RZ_NULLABLE void /* const struct winsize */ *win_params);
|
||||
RZ_API void rz_subprocess_close_pty(RZ_BORROW RZ_NONNULL const RzPty *pty);
|
||||
RZ_API bool rz_subprocess_login_tty(RZ_BORROW RZ_NONNULL const RzPty *pty);
|
||||
RZ_API void rz_subprocess_pty_free(RZ_OWN RzPty *pty);
|
||||
|
||||
#endif // RZ_UTIL_SUBPROCESS_H
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ RZ_API int rz_sys_fork(void);
|
|||
RZ_API int rz_sys_kill(int pid, int sig);
|
||||
// nocleanup = false => exit(); true => _exit()
|
||||
RZ_API void rz_sys_exit(int status, bool nocleanup);
|
||||
|
||||
/* openpty family of functions */
|
||||
RZ_API /* pid_t */ int rz_sys_forkpty(int *amaster, char *name, void /* const struct termios */ *termp, void /* const struct winsize */ *winp);
|
||||
RZ_API int rz_sys_openpty(int *amaster, int *aslave, char *name, void /* const struct termios */ *termp, void /* const struct winsize */ *winp);
|
||||
RZ_API int rz_sys_login_tty(int fd);
|
||||
|
||||
RZ_API bool rz_is_heap(void *p);
|
||||
RZ_API bool rz_sys_stop(void);
|
||||
RZ_API char *rz_sys_pid_to_path(int pid);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2020 Florian Märkl <info@florianmaerkl.de>
|
||||
// SPDX-FileCopyrightText: 2021 ret2libc <sirmy15@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2022 Dhruv Maroo <dhruvmaru007@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include <rz_cons.h>
|
||||
|
|
@ -246,7 +247,13 @@ error:
|
|||
return ret;
|
||||
}
|
||||
|
||||
RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt) {
|
||||
/**
|
||||
* \brief Start a subprocess, using the options provided in \p opt
|
||||
*
|
||||
* \param opt RzSubprocessOpt struct
|
||||
* \return RzSubprocess* The newly created subprocess
|
||||
*/
|
||||
RZ_API RZ_OWN RzSubprocess *rz_subprocess_start_opt(RZ_NONNULL const RzSubprocessOpt *opt) {
|
||||
RzSubprocess *proc = NULL;
|
||||
const HANDLE curr_stdin_handle = (HANDLE)_get_osfhandle(fileno(stdin));
|
||||
const HANDLE curr_stdout_handle = (HANDLE)_get_osfhandle(fileno(stdout));
|
||||
|
|
@ -700,6 +707,27 @@ RZ_API void rz_subprocess_free(RzSubprocess *proc) {
|
|||
CloseHandle(proc->proc);
|
||||
free(proc);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Unimplemented on Windows
|
||||
*
|
||||
* \return RzPty* NULL pointer until it has been implemented
|
||||
*/
|
||||
RZ_API RZ_OWN RzPty *rz_subprocess_openpty(RZ_BORROW RZ_NULLABLE char *slave_name, RZ_NULLABLE void /* const struct termios */ *term_params, RZ_NULLABLE void /* const struct winsize */ *win_params) {
|
||||
RZ_LOG_ERROR("openpty: Not implemented for Windows!");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Unimplemented on Windows
|
||||
*
|
||||
* \return bool false
|
||||
*/
|
||||
RZ_API bool rz_subprocess_login_tty(RZ_BORROW RZ_NONNULL const RzPty *pty) {
|
||||
RZ_LOG_ERROR("login_tty: Not implemented for Windows!");
|
||||
return false;
|
||||
}
|
||||
|
||||
#else // __WINDOWS__
|
||||
|
||||
#include <errno.h>
|
||||
|
|
@ -714,6 +742,8 @@ struct rz_subprocess_t {
|
|||
int ret;
|
||||
RzStrBuf out;
|
||||
RzStrBuf err;
|
||||
int master_fd; ///< Needed to check whether PTY
|
||||
int slave_fd;
|
||||
};
|
||||
|
||||
static RzPVector subprocs;
|
||||
|
|
@ -890,10 +920,109 @@ static void destroy_child_env(char **child_env) {
|
|||
free(child_env);
|
||||
}
|
||||
|
||||
RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt) {
|
||||
static bool init_pipes(RzSubprocess *proc, const RzSubprocessOpt *opt, int stdin_pipe[], int stdout_pipe[], int stderr_pipe[], RzPty *new_pty) {
|
||||
RzPty *pty = opt->pty;
|
||||
|
||||
/* If we need to use a PTY, we should create one right now */
|
||||
if (!pty && (opt->stdin_pipe == RZ_SUBPROCESS_PIPE_PTY || opt->stdout_pipe == RZ_SUBPROCESS_PIPE_PTY || opt->stderr_pipe == RZ_SUBPROCESS_PIPE_PTY)) {
|
||||
pty = rz_subprocess_openpty(NULL, NULL, NULL);
|
||||
if (!pty) {
|
||||
return false;
|
||||
}
|
||||
new_pty = pty;
|
||||
}
|
||||
|
||||
if (pty) {
|
||||
proc->master_fd = pty->master_fd;
|
||||
proc->slave_fd = pty->slave_fd;
|
||||
}
|
||||
|
||||
switch (opt->stdin_pipe) {
|
||||
case RZ_SUBPROCESS_PIPE_CREATE:
|
||||
if (rz_sys_pipe(stdin_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
return false;
|
||||
}
|
||||
proc->stdin_fd = stdin_pipe[1];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_PTY:
|
||||
stdin_pipe[0] = pty->slave_fd;
|
||||
stdin_pipe[1] = pty->master_fd;
|
||||
proc->stdin_fd = stdin_pipe[1];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_STDOUT:
|
||||
RZ_LOG_ERROR("Invalid pipe option 'RZ_SUBPROCESS_PIPE_STDOUT' for stdin. Ignoring...\n");
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_NONE:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (opt->stdout_pipe) {
|
||||
case RZ_SUBPROCESS_PIPE_CREATE:
|
||||
if (rz_sys_pipe(stdout_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
return false;
|
||||
}
|
||||
if (fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK) < 0) {
|
||||
perror("fcntl");
|
||||
return false;
|
||||
}
|
||||
proc->stdout_fd = stdout_pipe[0];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_PTY:
|
||||
stdout_pipe[0] = pty->master_fd;
|
||||
stdout_pipe[1] = pty->slave_fd;
|
||||
proc->stdout_fd = stdout_pipe[0];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_STDOUT:
|
||||
RZ_LOG_ERROR("Invalid pipe option 'RZ_SUBPROCESS_PIPE_STDOUT' for stdout. Ignoring...\n");
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_NONE:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (opt->stderr_pipe) {
|
||||
case RZ_SUBPROCESS_PIPE_CREATE:
|
||||
if (rz_sys_pipe(stderr_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
return false;
|
||||
}
|
||||
if (fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK) < 0) {
|
||||
perror("fcntl");
|
||||
return false;
|
||||
}
|
||||
proc->stderr_fd = stderr_pipe[0];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_PTY:
|
||||
stdout_pipe[0] = pty->master_fd;
|
||||
stdout_pipe[1] = pty->slave_fd;
|
||||
proc->stdout_fd = stdout_pipe[0];
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_STDOUT:
|
||||
stderr_pipe[0] = stdout_pipe[0];
|
||||
stderr_pipe[1] = stdout_pipe[1];
|
||||
proc->stderr_fd = proc->stdout_fd;
|
||||
break;
|
||||
case RZ_SUBPROCESS_PIPE_NONE:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Start a subprocess, using the options provided in \p opt
|
||||
*
|
||||
* \param opt RzSubprocessOpt struct
|
||||
* \return RzSubprocess* The newly created subprocess
|
||||
*/
|
||||
RZ_API RZ_OWN RzSubprocess *rz_subprocess_start_opt(RZ_NONNULL const RzSubprocessOpt *opt) {
|
||||
rz_return_val_if_fail(opt, NULL);
|
||||
|
||||
RzSubprocess *proc = NULL;
|
||||
char **child_env = NULL;
|
||||
char **argv = calloc(opt->args_size + 2, sizeof(char *));
|
||||
RzPty *new_pty = NULL;
|
||||
if (!argv) {
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -912,6 +1041,8 @@ RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt) {
|
|||
proc->stdin_fd = -1;
|
||||
proc->stdout_fd = -1;
|
||||
proc->stderr_fd = -1;
|
||||
proc->master_fd = -1;
|
||||
proc->slave_fd = -1;
|
||||
rz_strbuf_init(&proc->out);
|
||||
rz_strbuf_init(&proc->err);
|
||||
|
||||
|
|
@ -927,72 +1058,54 @@ RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt) {
|
|||
int stdin_pipe[2] = { -1, -1 };
|
||||
int stdout_pipe[2] = { -1, -1 };
|
||||
int stderr_pipe[2] = { -1, -1 };
|
||||
if (opt->stdin_pipe == RZ_SUBPROCESS_PIPE_CREATE) {
|
||||
if (rz_sys_pipe(stdin_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
goto error;
|
||||
}
|
||||
proc->stdin_fd = stdin_pipe[1];
|
||||
}
|
||||
|
||||
if (opt->stdout_pipe == RZ_SUBPROCESS_PIPE_CREATE) {
|
||||
if (rz_sys_pipe(stdout_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
goto error;
|
||||
}
|
||||
if (fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK) < 0) {
|
||||
perror("fcntl");
|
||||
goto error;
|
||||
}
|
||||
proc->stdout_fd = stdout_pipe[0];
|
||||
}
|
||||
|
||||
if (opt->stderr_pipe == RZ_SUBPROCESS_PIPE_CREATE) {
|
||||
if (rz_sys_pipe(stderr_pipe, true) == -1) {
|
||||
perror("pipe");
|
||||
goto error;
|
||||
}
|
||||
if (fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK) < 0) {
|
||||
perror("fcntl");
|
||||
goto error;
|
||||
}
|
||||
proc->stderr_fd = stderr_pipe[0];
|
||||
} else if (opt->stderr_pipe == RZ_SUBPROCESS_PIPE_STDOUT) {
|
||||
stderr_pipe[0] = stdout_pipe[0];
|
||||
stderr_pipe[1] = stdout_pipe[1];
|
||||
proc->stderr_fd = proc->stdout_fd;
|
||||
if (!init_pipes(proc, opt, stdin_pipe, stdout_pipe, stderr_pipe, new_pty)) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Let's create the environment for the child in the parent, with malloc,
|
||||
// because we can't use functions that lock after fork
|
||||
child_env = create_child_env(opt->envvars, opt->envvals, opt->env_size);
|
||||
|
||||
proc->pid = rz_sys_fork();
|
||||
|
||||
if (proc->pid == -1) {
|
||||
// fail
|
||||
perror("fork");
|
||||
goto error;
|
||||
} else if (proc->pid == 0) {
|
||||
// child
|
||||
|
||||
if (stderr_pipe[1] != -1) {
|
||||
while ((dup2(stderr_pipe[1], STDERR_FILENO) == -1) && (errno == EINTR)) {
|
||||
}
|
||||
if (proc->stderr_fd != proc->stdout_fd) {
|
||||
if (proc->stderr_fd != proc->stdout_fd && proc->stderr_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(stderr_pipe[1]);
|
||||
rz_sys_pipe_close(stderr_pipe[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (stdout_pipe[1] != -1) {
|
||||
while ((dup2(stdout_pipe[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {
|
||||
}
|
||||
rz_sys_pipe_close(stdout_pipe[1]);
|
||||
rz_sys_pipe_close(stdout_pipe[0]);
|
||||
if (proc->stdout_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(stdout_pipe[1]);
|
||||
rz_sys_pipe_close(stdout_pipe[0]);
|
||||
}
|
||||
}
|
||||
if (stdin_pipe[0] != -1) {
|
||||
while ((dup2(stdin_pipe[0], STDIN_FILENO) == -1) && (errno == EINTR)) {
|
||||
}
|
||||
rz_sys_pipe_close(stdin_pipe[0]);
|
||||
rz_sys_pipe_close(stdin_pipe[1]);
|
||||
if (proc->stdin_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(stdin_pipe[0]);
|
||||
rz_sys_pipe_close(stdin_pipe[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (proc->master_fd != -1 && close(proc->master_fd)) {
|
||||
perror("close");
|
||||
}
|
||||
if (proc->slave_fd != -1 && close(proc->slave_fd)) {
|
||||
perror("close");
|
||||
}
|
||||
|
||||
// Use the previously created environment
|
||||
|
|
@ -1005,18 +1118,45 @@ RZ_API RzSubprocess *rz_subprocess_start_opt(RzSubprocessOpt *opt) {
|
|||
destroy_child_env(child_env);
|
||||
free(argv);
|
||||
|
||||
if (stdin_pipe[0] != -1) {
|
||||
if (!opt->make_raw || proc->slave_fd == -1) {
|
||||
goto no_term_change;
|
||||
}
|
||||
|
||||
#if HAVE_FORKPTY && HAVE_OPENPTY && HAVE_LOGIN_TTY
|
||||
struct termios term_params;
|
||||
/* Needed to avoid reading back the writes again from the TTY */
|
||||
if (tcgetattr(proc->slave_fd, &term_params) != 0) {
|
||||
perror("tcgetattr");
|
||||
goto no_term_change;
|
||||
}
|
||||
cfmakeraw(&term_params);
|
||||
/* This avoids ECHO, so we don't read back whatever we wrote */
|
||||
if (tcsetattr(proc->slave_fd, TCSANOW, &term_params) != 0) {
|
||||
perror("tcsetattr");
|
||||
}
|
||||
#endif
|
||||
|
||||
no_term_change:
|
||||
if (proc->slave_fd != -1 && close(proc->slave_fd) == -1) {
|
||||
perror("close");
|
||||
}
|
||||
|
||||
if (new_pty) {
|
||||
/* Free the RzPTY if we created it */
|
||||
rz_subprocess_pty_free(new_pty);
|
||||
}
|
||||
|
||||
if (stdin_pipe[0] != -1 && stdin_pipe[0] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stdin_pipe[0]);
|
||||
}
|
||||
if (stdout_pipe[1] != -1) {
|
||||
if (stdout_pipe[1] != -1 && stdout_pipe[1] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stdout_pipe[1]);
|
||||
}
|
||||
if (stderr_pipe[1] != -1 && proc->stderr_fd != proc->stdout_fd) {
|
||||
if (stderr_pipe[1] != -1 && proc->stderr_fd != proc->stdout_fd && stderr_pipe[1] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stderr_pipe[1]);
|
||||
}
|
||||
|
||||
rz_pvector_push(&subprocs, proc);
|
||||
|
||||
subprocess_unlock();
|
||||
|
||||
return proc;
|
||||
|
|
@ -1028,41 +1168,54 @@ error:
|
|||
if (proc && proc->killpipe[1] == -1) {
|
||||
rz_sys_pipe_close(proc->killpipe[1]);
|
||||
}
|
||||
free(proc);
|
||||
if (stderr_pipe[0] != -1 && stderr_pipe[0] != stdout_pipe[0]) {
|
||||
if (stderr_pipe[0] != -1 && stderr_pipe[0] != stdout_pipe[0] && stderr_pipe[0] != proc->master_fd) {
|
||||
rz_sys_pipe_close(stderr_pipe[0]);
|
||||
}
|
||||
if (stderr_pipe[1] != -1 && stderr_pipe[1] != stdout_pipe[1]) {
|
||||
if (stderr_pipe[1] != -1 && stderr_pipe[1] != stdout_pipe[1] && stderr_pipe[0] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stderr_pipe[1]);
|
||||
}
|
||||
if (stdout_pipe[0] != -1) {
|
||||
if (stdout_pipe[0] != -1 && stdout_pipe[0] != proc->master_fd) {
|
||||
rz_sys_pipe_close(stdout_pipe[0]);
|
||||
}
|
||||
if (stdout_pipe[1] != -1) {
|
||||
if (stdout_pipe[1] != -1 && stdout_pipe[1] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stdout_pipe[1]);
|
||||
}
|
||||
if (stdin_pipe[0] != -1) {
|
||||
if (stdin_pipe[0] != -1 && stdin_pipe[0] != proc->slave_fd) {
|
||||
rz_sys_pipe_close(stdin_pipe[0]);
|
||||
}
|
||||
if (stdin_pipe[1] != -1) {
|
||||
if (stdin_pipe[1] != -1 && stdin_pipe[1] != proc->master_fd) {
|
||||
rz_sys_pipe_close(stdin_pipe[1]);
|
||||
}
|
||||
if (proc->master_fd != -1) {
|
||||
close(proc->master_fd);
|
||||
}
|
||||
if (proc->slave_fd != -1) {
|
||||
close(proc->slave_fd);
|
||||
}
|
||||
free(proc);
|
||||
|
||||
if (new_pty) {
|
||||
/* Free the RzPTY if we created it */
|
||||
RZ_FREE(new_pty);
|
||||
}
|
||||
|
||||
destroy_child_env(child_env);
|
||||
subprocess_unlock();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static size_t read_to_strbuf(RzStrBuf *sb, int fd, bool *fd_eof, size_t n_bytes) {
|
||||
static size_t read_to_strbuf(RzStrBuf *sb, int fd, bool *fd_eof, size_t n_bytes, bool is_pty) {
|
||||
char buf[BUFFER_SIZE];
|
||||
size_t to_read = sizeof(buf);
|
||||
if (n_bytes && to_read > n_bytes) {
|
||||
to_read = n_bytes;
|
||||
}
|
||||
ssize_t sz = read(fd, buf, to_read);
|
||||
if (sz < 0) {
|
||||
perror("read");
|
||||
} else if (sz == 0) {
|
||||
if (sz == 0 || (is_pty && sz == -1 && errno == EIO)) {
|
||||
/* In case of PTY, EIO (input/output error) denotes EOF, hence the manual checking */
|
||||
*fd_eof = true;
|
||||
} else if (sz < 0) {
|
||||
perror("read");
|
||||
} else {
|
||||
rz_strbuf_append_n(sb, buf, (int)sz);
|
||||
}
|
||||
|
|
@ -1096,6 +1249,11 @@ static RzSubprocessWaitReason subprocess_wait(RzSubprocess *proc, ut64 timeout_m
|
|||
bool child_dead = false;
|
||||
bool timedout = true;
|
||||
bool bytes_enabled = n_bytes != 0;
|
||||
|
||||
/* Check if stdout and stderr are connected to a PTY */
|
||||
bool stdout_pty = proc->stdout_fd != -1 && proc->stdout_fd == proc->master_fd;
|
||||
bool stderr_pty = proc->stderr_fd != -1 && proc->stderr_fd == proc->master_fd;
|
||||
|
||||
while ((!bytes_enabled || n_bytes) && ((stdout_enabled && !stdout_eof) || (stderr_enabled && !stderr_eof) || !child_dead)) {
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
|
|
@ -1143,14 +1301,14 @@ static RzSubprocessWaitReason subprocess_wait(RzSubprocess *proc, ut64 timeout_m
|
|||
timedout = true;
|
||||
if (stdout_enabled && FD_ISSET(proc->stdout_fd, &rfds)) {
|
||||
timedout = false;
|
||||
size_t r = read_to_strbuf(&proc->out, proc->stdout_fd, &stdout_eof, n_bytes);
|
||||
size_t r = read_to_strbuf(&proc->out, proc->stdout_fd, &stdout_eof, n_bytes, stdout_pty);
|
||||
if (r > 0 && n_bytes) {
|
||||
n_bytes -= r;
|
||||
}
|
||||
}
|
||||
if (stderr_enabled && FD_ISSET(proc->stderr_fd, &rfds)) {
|
||||
timedout = false;
|
||||
size_t r = read_to_strbuf(&proc->err, proc->stderr_fd, &stderr_eof, n_bytes);
|
||||
size_t r = read_to_strbuf(&proc->err, proc->stderr_fd, &stderr_eof, n_bytes, stderr_pty);
|
||||
if (r > 0 && n_bytes) {
|
||||
n_bytes -= r;
|
||||
}
|
||||
|
|
@ -1183,7 +1341,9 @@ static RzSubprocessWaitReason subprocess_wait(RzSubprocess *proc, ut64 timeout_m
|
|||
* \param timeout_ms Wait for at most this amount of millisecond
|
||||
*/
|
||||
RZ_API RzSubprocessWaitReason rz_subprocess_wait(RzSubprocess *proc, ut64 timeout_ms) {
|
||||
if (proc->stdin_fd != -1) {
|
||||
/* Should not close proc->stdin_fd if the fork mode was PTY
|
||||
(because it might point to the master fd, which needs to stay open to get the std{out,err}) */
|
||||
if (proc->stdin_fd != -1 && proc->stdin_fd != proc->master_fd) {
|
||||
// Close subprocess stdin to tell it that no more input will come from us
|
||||
rz_sys_pipe_close(proc->stdin_fd);
|
||||
proc->stdin_fd = -1;
|
||||
|
|
@ -1238,7 +1398,7 @@ RZ_API RzStrBuf *rz_subprocess_stdout_read(RzSubprocess *proc, size_t n, ut64 ti
|
|||
* \param proc Subprocess to communicate with
|
||||
* \param timeout_ms Wait for at most this amount of millisecond to read subprocess' stdout
|
||||
*/
|
||||
RZ_API RzStrBuf *rz_subprocess_stdout_readline(RzSubprocess *proc, ut64 timeout_ms) {
|
||||
RZ_API RZ_BORROW RzStrBuf *rz_subprocess_stdout_readline(RzSubprocess *proc, ut64 timeout_ms) {
|
||||
rz_strbuf_fini(&proc->out);
|
||||
rz_strbuf_init(&proc->out);
|
||||
if (proc->stdout_fd != -1) {
|
||||
|
|
@ -1281,17 +1441,98 @@ RZ_API void rz_subprocess_free(RzSubprocess *proc) {
|
|||
rz_strbuf_fini(&proc->err);
|
||||
rz_sys_pipe_close(proc->killpipe[0]);
|
||||
rz_sys_pipe_close(proc->killpipe[1]);
|
||||
if (proc->stdin_fd != -1) {
|
||||
|
||||
if (proc->master_fd != -1) {
|
||||
rz_sys_pipe_close(proc->master_fd);
|
||||
}
|
||||
if (proc->stdin_fd != -1 && proc->stdin_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(proc->stdin_fd);
|
||||
}
|
||||
if (proc->stdout_fd != -1) {
|
||||
if (proc->stdout_fd != -1 && proc->stdout_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(proc->stdout_fd);
|
||||
}
|
||||
if (proc->stderr_fd != -1 && proc->stderr_fd != proc->stdout_fd) {
|
||||
if (proc->stderr_fd != -1 && proc->stderr_fd != proc->stdout_fd && proc->stderr_fd != proc->master_fd) {
|
||||
rz_sys_pipe_close(proc->stderr_fd);
|
||||
}
|
||||
free(proc);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Call openpty(3) with the provided arguments
|
||||
*
|
||||
* \param slave_name The name of the slave PTY is stored in this
|
||||
* This is marked as RZ_BORROW, so it's ownership no longer stays with the caller
|
||||
* and is now owned by the returned RzPty struct
|
||||
*
|
||||
* \param term_params Terminal attributes (struct termios) for the forked process
|
||||
* \param win_params Window attributes (struct winsize) for the forked process
|
||||
*
|
||||
* \return RzPty*
|
||||
*/
|
||||
RZ_API RZ_OWN RzPty *rz_subprocess_openpty(RZ_BORROW RZ_NULLABLE char *slave_name, RZ_NULLABLE void /* const struct termios */ *term_params, RZ_NULLABLE void /* const struct winsize */ *win_params) {
|
||||
RzPty *pty = RZ_NEW0(RzPty);
|
||||
int ret = rz_sys_openpty(&pty->master_fd, &pty->slave_fd, slave_name, NULL, NULL);
|
||||
|
||||
if (ret == -1) {
|
||||
perror("openpty");
|
||||
RZ_FREE(pty);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pty;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Call login_tty(3) on the provided \p pty
|
||||
*
|
||||
* \param pty RzPty struct
|
||||
* \return bool true if login_tty succeeded, false otherwise
|
||||
*/
|
||||
RZ_API bool rz_subprocess_login_tty(RZ_BORROW RZ_NONNULL const RzPty *pty) {
|
||||
rz_return_val_if_fail(pty, false);
|
||||
|
||||
int ret = rz_sys_login_tty(pty->slave_fd);
|
||||
if (ret == -1) {
|
||||
perror("login_tty");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Closes the file descriptors associated with \p pty
|
||||
*
|
||||
* \param pty RzPty struct
|
||||
* \return void
|
||||
*
|
||||
* No need to call this after you've used the \p pty in `rz_subprocess_start_opt`,
|
||||
* since the file descriptors would already have been correctly closed
|
||||
*/
|
||||
RZ_API void rz_subprocess_close_pty(RZ_BORROW RZ_NONNULL const RzPty *pty) {
|
||||
if (close(pty->master_fd) == -1) {
|
||||
perror("close");
|
||||
}
|
||||
if (close(pty->slave_fd) == -1) {
|
||||
perror("close");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Free the \p pty
|
||||
*
|
||||
* \param pty RzPty struct
|
||||
* \return void
|
||||
*/
|
||||
RZ_API void rz_subprocess_pty_free(RZ_OWN RzPty *pty) {
|
||||
if (!pty) {
|
||||
return;
|
||||
}
|
||||
|
||||
RZ_FREE(pty->name);
|
||||
free(pty);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
RZ_API int rz_subprocess_ret(RzSubprocess *proc) {
|
||||
|
|
@ -1355,6 +1596,8 @@ RZ_API RzSubprocess *rz_subprocess_start(
|
|||
.stdin_pipe = RZ_SUBPROCESS_PIPE_CREATE,
|
||||
.stdout_pipe = RZ_SUBPROCESS_PIPE_CREATE,
|
||||
.stderr_pipe = RZ_SUBPROCESS_PIPE_CREATE,
|
||||
.pty = NULL,
|
||||
.make_raw = /* does not matter */ false
|
||||
};
|
||||
return rz_subprocess_start_opt(&opt);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ extern char **environ;
|
|||
#endif
|
||||
#endif
|
||||
|
||||
/* For "openpty" family of funtcions */
|
||||
#if HAVE_OPENPTY && HAVE_FORKPTY && HAVE_LOGIN_TTY
|
||||
#if defined(__APPLE__) || defined(__NetBSD__) || defined(__OpenBSD__)
|
||||
#include <util.h>
|
||||
#elif defined(__FreeBSD__) || defined(__DragonFly__)
|
||||
#include <libutil.h>
|
||||
#else
|
||||
#include <pty.h>
|
||||
#include <utmp.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
RZ_LIB_VERSION(rz_util);
|
||||
|
||||
#ifdef __x86_64__
|
||||
|
|
@ -1666,6 +1678,9 @@ RZ_API int rz_sys_fork(void) {
|
|||
parent_lock_enter();
|
||||
#endif
|
||||
pid_t child = fork();
|
||||
if (child == -1) {
|
||||
perror("fork");
|
||||
}
|
||||
#if __UNIX__ && HAVE_PIPE && !HAVE_PIPE2
|
||||
if (child == 0) {
|
||||
is_child = true;
|
||||
|
|
@ -1681,6 +1696,72 @@ RZ_API int rz_sys_fork(void) {
|
|||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Wrapper for forkpty(3)
|
||||
*
|
||||
* \param amaster The master end of the PTY is stored here
|
||||
* \param name The name of the slave end of the PTY is stored here
|
||||
* \param termp (const struct termios) The terminal attributes
|
||||
* \param winp (const struct winsize) The window size attributes
|
||||
*
|
||||
* \return int (pid_t) PID of the forked process
|
||||
*/
|
||||
RZ_API /* pid_t */ int rz_sys_forkpty(int *amaster, char *name, void /* const struct termios */ *termp, void /* const struct winsize */ *winp) {
|
||||
#if HAVE_OPENPTY && HAVE_FORKPTY && HAVE_LOGIN_TTY
|
||||
pid_t ret = forkpty(amaster, name, termp, winp);
|
||||
if (ret == -1) {
|
||||
perror("forkpty");
|
||||
}
|
||||
return ret;
|
||||
#else
|
||||
RZ_LOG_ERROR("forkpty() not found\n");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Wrapper for openpty(3)
|
||||
*
|
||||
* \param amaster The master end of the PTY is stored here
|
||||
* \param aslave The slave end of the PTY is stored here
|
||||
* \param name The name of the slave end of the PTY is stored here
|
||||
* \param termp (const struct termios) The terminal attributes
|
||||
* \param winp (const struct winsize) The window size attributes
|
||||
*
|
||||
* \return int Return code
|
||||
*/
|
||||
RZ_API int rz_sys_openpty(int *amaster, int *aslave, char *name, void /* const struct termios */ *termp, void /* const struct winsize */ *winp) {
|
||||
#if HAVE_OPENPTY && HAVE_FORKPTY && HAVE_LOGIN_TTY
|
||||
int ret = openpty(amaster, aslave, name, termp, winp);
|
||||
if (ret == -1) {
|
||||
perror("openpty");
|
||||
}
|
||||
return ret;
|
||||
#else
|
||||
RZ_LOG_ERROR("openpty() not found\n");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Wrapper for login_tty(3)
|
||||
*
|
||||
* \param fd File descriptor for the slave end of the PTY; To be made the controlling terminal
|
||||
* \return int Return code
|
||||
*/
|
||||
RZ_API int rz_sys_login_tty(int fd) {
|
||||
#if HAVE_OPENPTY && HAVE_FORKPTY && HAVE_LOGIN_TTY
|
||||
int ret = login_tty(fd);
|
||||
if (ret == -1) {
|
||||
perror("login_tty");
|
||||
}
|
||||
return ret;
|
||||
#else
|
||||
RZ_LOG_ERROR("login_tty() not found\n");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
RZ_API int rz_sys_truncate_fd(int fd, ut64 length) {
|
||||
#ifdef _MSC_VER
|
||||
return _chsize_s(fd, length);
|
||||
|
|
|
|||
|
|
@ -263,6 +263,131 @@ bool test_interactive(void) {
|
|||
mu_end;
|
||||
}
|
||||
|
||||
#if HAVE_FORKPTY && HAVE_OPENPTY && HAVE_LOGIN_TTY
|
||||
bool test_interactive_pty(void) {
|
||||
rz_subprocess_init();
|
||||
const char *exe_path = get_auxiliary_path("subprocess-interactive");
|
||||
|
||||
RzSubprocessOpt opt = { 0 };
|
||||
opt.file = exe_path;
|
||||
/* Try all PTY */
|
||||
opt.stdin_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.stdout_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.stderr_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.make_raw = true;
|
||||
|
||||
RzSubprocess *sp = rz_subprocess_start_opt(&opt);
|
||||
mu_assert_notnull(sp, "the subprocess should be created");
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"3\n", strlen("3\n"));
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"5\n", strlen("5\n"));
|
||||
|
||||
RzStrBuf *sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
int c = atoi(rz_strbuf_get(sb));
|
||||
char buf[100];
|
||||
snprintf(buf, sizeof(buf), "%d\n", 3 + 5 + c);
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)buf, strlen(buf));
|
||||
|
||||
rz_subprocess_wait(sp, UT_TIMEOUT);
|
||||
RzSubprocessOutput *spo = rz_subprocess_drain(sp);
|
||||
mu_assert_streq(remove_cr(spo->out), "Right\n", "A Good message should be returned");
|
||||
mu_assert_eq(spo->ret, 0, "subprocess exited in the right way");
|
||||
|
||||
rz_subprocess_output_free(spo);
|
||||
rz_subprocess_free(sp);
|
||||
rz_subprocess_fini();
|
||||
mu_end;
|
||||
}
|
||||
|
||||
bool test_interactive_custom_pty(void) {
|
||||
rz_subprocess_init();
|
||||
const char *exe_path = get_auxiliary_path("subprocess-interactive");
|
||||
|
||||
RzSubprocessOpt opt = { 0 };
|
||||
opt.file = exe_path;
|
||||
opt.stdin_pipe = RZ_SUBPROCESS_PIPE_CREATE;
|
||||
opt.stdout_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.stderr_pipe = RZ_SUBPROCESS_PIPE_STDOUT;
|
||||
opt.make_raw = true;
|
||||
opt.pty = rz_subprocess_openpty(NULL, NULL, NULL);
|
||||
|
||||
RzSubprocess *sp = rz_subprocess_start_opt(&opt);
|
||||
mu_assert_notnull(sp, "the subprocess should be created");
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"3\n", strlen("3\n"));
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"5\n", strlen("5\n"));
|
||||
|
||||
RzStrBuf *sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
int c = atoi(rz_strbuf_get(sb));
|
||||
char buf[100];
|
||||
snprintf(buf, sizeof(buf), "%d\n", 3 + 5 + c);
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)buf, strlen(buf));
|
||||
|
||||
rz_subprocess_wait(sp, UT_TIMEOUT);
|
||||
RzSubprocessOutput *spo = rz_subprocess_drain(sp);
|
||||
mu_assert_streq(remove_cr(spo->out), "Right\n", "A Good message should be returned");
|
||||
mu_assert_eq(spo->ret, 0, "subprocess exited in the right way");
|
||||
|
||||
rz_subprocess_output_free(spo);
|
||||
rz_subprocess_free(sp);
|
||||
rz_subprocess_fini();
|
||||
rz_subprocess_pty_free(opt.pty);
|
||||
mu_end;
|
||||
}
|
||||
|
||||
bool test_interactive_not_raw_pty(void) {
|
||||
rz_subprocess_init();
|
||||
const char *exe_path = get_auxiliary_path("subprocess-interactive");
|
||||
|
||||
RzSubprocessOpt opt = { 0 };
|
||||
opt.file = exe_path;
|
||||
opt.stdin_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.stdout_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
opt.stderr_pipe = RZ_SUBPROCESS_PIPE_PTY;
|
||||
/* Not raw, so all input will be echoe-ed */
|
||||
opt.make_raw = false;
|
||||
|
||||
RzSubprocess *sp = rz_subprocess_start_opt(&opt);
|
||||
mu_assert_notnull(sp, "the subprocess should be created");
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"3\n", strlen("3\n"));
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)"5\n", strlen("5\n"));
|
||||
|
||||
/* CRLF is returned, can be changed by using custom PTY with custom term params
|
||||
Not necessary for testing here though */
|
||||
RzStrBuf *sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
mu_assert_streq(remove_cr(rz_strbuf_get(sb)), "3\r\n", "No 3");
|
||||
sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
mu_assert_streq(remove_cr(rz_strbuf_get(sb)), "5\r\n", "No 5");
|
||||
|
||||
sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
int c = atoi(rz_strbuf_get(sb));
|
||||
char buf[100];
|
||||
snprintf(buf, sizeof(buf), "%d\n", 3 + 5 + c);
|
||||
rz_subprocess_stdin_write(sp, (const ut8 *)buf, strlen(buf));
|
||||
|
||||
sb = rz_subprocess_stdout_readline(sp, UT_TIMEOUT);
|
||||
mu_assert_eq(atoi(rz_strbuf_get(sb)), 3 + 5 + c, "No 3 + 5 + c");
|
||||
|
||||
rz_subprocess_wait(sp, UT_TIMEOUT);
|
||||
RzSubprocessOutput *spo = rz_subprocess_drain(sp);
|
||||
mu_assert_streq(remove_cr(spo->out), "Right\r\n", "A Good message should be returned");
|
||||
mu_assert_eq(spo->ret, 0, "subprocess exited in the right way");
|
||||
|
||||
rz_subprocess_output_free(spo);
|
||||
rz_subprocess_free(sp);
|
||||
rz_subprocess_fini();
|
||||
mu_end;
|
||||
}
|
||||
#else
|
||||
bool test_interactive_pty(void) {
|
||||
mu_end;
|
||||
}
|
||||
bool test_interactive_custom_pty(void) {
|
||||
mu_end;
|
||||
}
|
||||
bool test_interactive_not_raw_pty(void) {
|
||||
mu_end;
|
||||
}
|
||||
#endif // PTY functions
|
||||
|
||||
bool all_tests() {
|
||||
mu_run_test(test_noargs_noinput_outerr);
|
||||
mu_run_test(test_args);
|
||||
|
|
@ -275,7 +400,10 @@ bool all_tests() {
|
|||
mu_run_test(test_stderronly);
|
||||
mu_run_test(test_stdoutstderr);
|
||||
mu_run_test(test_interactive);
|
||||
mu_run_test(test_interactive_pty);
|
||||
mu_run_test(test_interactive_custom_pty);
|
||||
mu_run_test(test_interactive_not_raw_pty);
|
||||
return tests_passed != tests_run;
|
||||
}
|
||||
|
||||
mu_main(all_tests)
|
||||
mu_main(all_tests)
|
||||
|
|
|
|||
Loading…
Reference in a new issue