From 57dd00954d68feea0d18dfff26556f1f09684ab0 Mon Sep 17 00:00:00 2001 From: Ben Gamari Date: Thu, 24 Jun 2021 14:12:04 -0400 Subject: [PATCH 1/4] Fix testsuite on non-Windows platforms --- test/main.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/main.hs b/test/main.hs index b7c5627d..27fbdca8 100644 --- a/test/main.hs +++ b/test/main.hs @@ -13,6 +13,12 @@ import System.IO (hClose, openBinaryTempFile, hGetContents) import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8 import System.Directory (getTemporaryDirectory, removeFile) +import System.Info (os) + +ifWindows :: IO () -> IO () +ifWindows action + | os /= "windows" = return () + | otherwise = action main :: IO () main = do @@ -40,7 +46,7 @@ main = do putStrLn "Testing subdirectories" - withCurrentDirectory "exes" $ do + ifWindows $ withCurrentDirectory "exes" $ do res1 <- readCreateProcess (proc "./echo.bat" []) "" unless ("parent" `isInfixOf` res1 && not ("child" `isInfixOf` res1)) $ error $ "echo.bat with cwd failed: " ++ show res1 From c610bb0a8535e8d5dd82210e02f34f552e53b669 Mon Sep 17 00:00:00 2001 From: Ben Gamari Date: Mon, 20 Apr 2020 18:38:15 -0400 Subject: [PATCH 2/4] runProcess: Check for malloc failure on Windows --- cbits/win32/runProcess.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cbits/win32/runProcess.c b/cbits/win32/runProcess.c index e11c47c6..22c8c6df 100644 --- a/cbits/win32/runProcess.c +++ b/cbits/win32/runProcess.c @@ -553,6 +553,10 @@ waitForJobCompletion ( HANDLE hJob ) if (pid_list == NULL) { pid_list = malloc(pid_list_size); + if (pid_list == NULL) { + errno = ENOMEM; + return false; + } pid_list->NumberOfAssignedProcesses = process_count; } From 30076bbc30f1296563a118798a40eae20d313fe4 Mon Sep 17 00:00:00 2001 From: Ben Gamari Date: Thu, 24 Jun 2021 00:18:33 -0400 Subject: [PATCH 3/4] posix: Move RESET_INT_QUIT_HANDLERS to flags Previously this was a separate argument despite the fact that we already pass a flags argument. --- System/Process/Posix.hs | 15 ++++++++------- cbits/posix/runProcess.c | 3 +-- include/processFlags.h | 1 + include/runProcess.h | 1 - 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/System/Process/Posix.hs b/System/Process/Posix.hs index f99eae9b..53c47852 100644 --- a/System/Process/Posix.hs +++ b/System/Process/Posix.hs @@ -139,18 +139,20 @@ createProcess_Internal fun when mb_delegate_ctlc startDelegateControlC + let flags = (if mb_close_fds then RUN_PROCESS_IN_CLOSE_FDS else 0) + .|.(if mb_create_group then RUN_PROCESS_IN_NEW_GROUP else 0) + .|.(if mb_detach_console then RUN_PROCESS_DETACHED else 0) + .|.(if mb_create_new_console then RUN_PROCESS_NEW_CONSOLE else 0) + .|.(if mb_new_session then RUN_PROCESS_NEW_SESSION else 0) + .|.(if mb_delegate_ctlc then RESET_INT_QUIT_HANDLERS else 0) + -- See the comment on runInteractiveProcess_lock proc_handle <- withMVar runInteractiveProcess_lock $ \_ -> c_runInteractiveProcess pargs pWorkDir pEnv fdin fdout fderr pfdStdInput pfdStdOutput pfdStdError pChildGroup pChildUser - (if mb_delegate_ctlc then 1 else 0) - ((if mb_close_fds then RUN_PROCESS_IN_CLOSE_FDS else 0) - .|.(if mb_create_group then RUN_PROCESS_IN_NEW_GROUP else 0) - .|.(if mb_detach_console then RUN_PROCESS_DETACHED else 0) - .|.(if mb_create_new_console then RUN_PROCESS_NEW_CONSOLE else 0) - .|.(if mb_new_session then RUN_PROCESS_NEW_SESSION else 0)) + flags pFailedDoing when (proc_handle == -1) $ do @@ -273,7 +275,6 @@ foreign import ccall unsafe "runInteractiveProcess" -> Ptr FD -> Ptr CGid -> Ptr CUid - -> CInt -- reset child's SIGINT & SIGQUIT handlers -> CInt -- flags -> Ptr CString -> IO PHANDLE diff --git a/cbits/posix/runProcess.c b/cbits/posix/runProcess.c index 61eb7884..7be513be 100644 --- a/cbits/posix/runProcess.c +++ b/cbits/posix/runProcess.c @@ -59,7 +59,6 @@ runInteractiveProcess (char *const args[], int fdStdIn, int fdStdOut, int fdStdErr, int *pfdStdInput, int *pfdStdOutput, int *pfdStdError, gid_t *childGroup, uid_t *childUser, - int reset_int_quit_handlers, int flags, char **failed_doing) { @@ -274,7 +273,7 @@ runInteractiveProcess (char *const args[], /* Reset the SIGINT/SIGQUIT signal handlers in the child, if requested */ - if (reset_int_quit_handlers) { + if (flags & RESET_INT_QUIT_HANDLES != 0) { struct sigaction dfl; (void)sigemptyset(&dfl.sa_mask); dfl.sa_flags = 0; diff --git a/include/processFlags.h b/include/processFlags.h index a2c4b954..711a1661 100644 --- a/include/processFlags.h +++ b/include/processFlags.h @@ -9,3 +9,4 @@ #define RUN_PROCESS_DETACHED 0x4 #define RUN_PROCESS_NEW_SESSION 0x8 #define RUN_PROCESS_NEW_CONSOLE 0x10 +#define RESET_INT_QUIT_HANDLERS 0x20 \ No newline at end of file diff --git a/include/runProcess.h b/include/runProcess.h index d1fb95c0..0ad05a65 100644 --- a/include/runProcess.h +++ b/include/runProcess.h @@ -69,7 +69,6 @@ extern ProcHandle runInteractiveProcess( char *const args[], int *pfdStdError, gid_t *childGroup, uid_t *childUser, - int reset_int_quit_handlers, int flags, char **failed_doing); From c7e5b062e03e2ddfc4602526142d793056c4e7f3 Mon Sep 17 00:00:00 2001 From: Ben Gamari Date: Wed, 23 Jun 2021 23:59:54 -0400 Subject: [PATCH 4/4] cbits/posix: Introduce posix_spawn support This is a complete rewrite `posix/runProcess.c`. There are a few goals of this rewrite: * fix a long-standing and serious bug in the `execvpe` fallback path, which uses non-reentrant functions after `fork`ing. This is of course undefined behavior and has been causing failures under Darwin's Rosetta binary translation engine (see GHC #19994). * eliminate code duplication in the `fork/exec` implementation. * introduce support for `posix_spawn`, allowing us to unload a significant amount of complexity in some cases. This is particularly desireable as the cost of `fork` has increased considerably in some cases on recent Darwin releases (namely when `MAP_JIT` mappings are used; see [1]) While `posix_spawn` is often a win, there are unfortunately several cases where it cannot be used: * `posix_spawn_file_actions_addchdir_np` is broken on Darwin * `POSIX_SPAWN_SETSID` is only supported on mac 10.15 and later, but doesn't return a proper error code when not supported * the originally-specified semantics of `posix_spawn_file_actions_adddup2` are unsafe and have been amended (see [3]) but not all implementations have caught up (musl has [4], glibc did later [5], Darwin seemingly hasn't) there appears to be no support at all for setuid and setgid * `spawn` is significantly slower than fork on some Darwin releases (see [6]) To address this we first try using `posix_spawn`, falling back on `fork/exec` if we encounter a case which the former cannot handle. [1]: https://github.com/libuv/libuv/pull/3064 [2]: https://www.austingroupbugs.net/view.php?id=411 [3]: https://github.com/rust-lang/rust/pull/80537 [4]: https://git.musl-libc.org/cgit/musl/commit/?id=6fc6ca1a323bc0b6b9e9cdc8fa72221ae18fe206 [5]: https://sourceware.org/bugzilla/show_bug.cgi?id=23640 [6]: https://discuss.python.org/t/multiprocessing-spawn-default-on-macos-since-python-3-8-is-slower-than-fork-method/5910/4 --- cbits/posix/common.h | 52 ++++ cbits/posix/find_executable.c | 79 +++++ cbits/posix/fork_exec.c | 312 ++++++++++++++++++++ cbits/posix/posix_spawn.c | 211 ++++++++++++++ cbits/posix/runProcess.c | 529 ++++++++++------------------------ configure.ac | 7 + include/runProcess.h | 34 +-- process.cabal | 3 + tests/all.T | 8 +- tests/process004.stdout | 4 +- tests/process010.stdout | 2 +- 11 files changed, 837 insertions(+), 404 deletions(-) create mode 100644 cbits/posix/common.h create mode 100644 cbits/posix/find_executable.c create mode 100644 cbits/posix/fork_exec.c create mode 100644 cbits/posix/posix_spawn.c diff --git a/cbits/posix/common.h b/cbits/posix/common.h new file mode 100644 index 00000000..6846b18e --- /dev/null +++ b/cbits/posix/common.h @@ -0,0 +1,52 @@ +#pragma once + +#include "runProcess.h" + +enum std_handle_behavior { + // Close the handle + STD_HANDLE_CLOSE, + // dup2 the specified fd to standard handle + STD_HANDLE_USE_FD, + // dup2 the appropriate end of the given pipe to the standard handle and + // close the other end. + STD_HANDLE_USE_PIPE +}; + +struct std_handle { + enum std_handle_behavior behavior; + union { + int use_fd; + struct { + int parent_end, child_end; + } use_pipe; + }; +}; + +int get_max_fd(void); + +// defined in find_executable.c +#if !defined(HAVE_execvpe) +char *find_executable(char *filename); +#endif + +// defined in fork_exec.c +ProcHandle +do_spawn_fork (char *const args[], + char *workingDirectory, char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing); + +// defined in posix_spawn.c +ProcHandle +do_spawn_posix (char *const args[], + char *workingDirectory, char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing); \ No newline at end of file diff --git a/cbits/posix/find_executable.c b/cbits/posix/find_executable.c new file mode 100644 index 00000000..d12ebca3 --- /dev/null +++ b/cbits/posix/find_executable.c @@ -0,0 +1,79 @@ +/* ---------------------------------------------------------------------------- + * search path search logic + * (c) Ben Gamari 2021 + */ + +#include +#include +#include +#include +#include + +#include "common.h" + +// the below is only necessary when we don't have execvpe. +#if !defined(HAVE_execvpe) + +/* Return true if the given file exists and is an executable. */ +static bool is_executable(const char *path) { + return access(path, X_OK) == 0; +} + +/* Find an executable with the given filename in the given search path. The + * result must be freed by the caller. Returns NULL if a matching file is not + * found. + */ +static char *find_in_search_path(char *search_path, const char *filename) { + const int filename_len = strlen(filename); + char *tokbuf; + char *path = strtok_r(search_path, ":", &tokbuf); + while (path != NULL) { + const int tmp_len = filename_len + 1 + strlen(path) + 1; + char *tmp = malloc(tmp_len); + snprintf(tmp, tmp_len, "%s/%s", path, filename); + if (is_executable(tmp)) { + return tmp; + } else { + free(tmp); + } + + path = strtok_r(NULL, ":", &tokbuf); + } + return NULL; +} + +/* Identify the executable search path. The result must be freed by the caller. */ +static char *get_executable_search_path(void) { + char *search_path; + + search_path = getenv("PATH"); + if (search_path) { + search_path = strdup(search_path); + return search_path; + } + +#if defined(HAVE_CONFSTR) + int len = confstr(_CS_PATH, NULL, 0); + search_path = malloc(len + 1) + if (search_path != NULL) { + search_path[0] = ':'; + (void) confstr (_CS_PATH, search_path + 1, len); + return search_path; + } +#endif + + return strdup(":"); +} + +/* Find the given executable in the executable search path. */ +char *find_executable(char *filename) { + /* If it's an absolute or relative path name, it's easy. */ + if (strchr(filename, '/') && is_executable(filename)) { + return filename; + } + + char *search_path = get_executable_search_path(); + return find_in_search_path(search_path, filename); +} + +#endif \ No newline at end of file diff --git a/cbits/posix/fork_exec.c b/cbits/posix/fork_exec.c new file mode 100644 index 00000000..282ee9c2 --- /dev/null +++ b/cbits/posix/fork_exec.c @@ -0,0 +1,312 @@ +#include "common.h" + +#include +#include +#include +#include +#include + +#if !(defined(_MSC_VER) || defined(__MINGW32__) || defined(_WIN32)) +#include +#include +#endif + +#if defined(HAVE_FCNTL_H) +#include +#endif + +#if defined(HAVE_SIGNAL_H) +#include +#endif + +#if defined(HAVE_VFORK_H) +#include +#endif + +#include + +#if defined(HAVE_WORKING_VFORK) +#define myfork vfork +#elif defined(HAVE_WORKING_FORK) +#define myfork fork +// We don't need a fork command on Windows +#else +#error Cannot find a working fork command +#endif + +// Rts internal API, not exposed in a public header file: +extern void blockUserSignals(void); +extern void unblockUserSignals(void); + +__attribute__((__noreturn__)) +static void +child_failed(int pipe, const char *failed_doing) { + int err; + ssize_t unused __attribute__((unused)); + + err = errno; + // Having the child send the failed_doing pointer across the pipe is safe as + // we know that the child still has the same address space as the parent. + unused = write(pipe, &failed_doing, sizeof(failed_doing)); + unused = write(pipe, &err, sizeof(err)); + // As a fallback, exit + _exit(127); +} + +static int +setup_std_handle_fork(int fd, + struct std_handle *b, + int pipe) +{ + switch (b->behavior) { + case STD_HANDLE_CLOSE: + if (close(fd) == -1) { + child_failed(pipe, "close"); + return -1; + } + return 0; + + case STD_HANDLE_USE_FD: + if (dup2(b->use_fd, fd) == -1) { + child_failed(pipe, "dup2"); + } + return 0; + + case STD_HANDLE_USE_PIPE: + if (b->use_pipe.child_end != fd) { + if (dup2(b->use_pipe.child_end, fd) == -1) { + child_failed(pipe, "dup2(child_end)"); + } + if (close(b->use_pipe.child_end) == -1) { + child_failed(pipe, "close(child_end)"); + } + } + if (close(b->use_pipe.parent_end) == -1) { + child_failed(pipe, "close(parent_end)"); + } + break; + } +} + +/* Try spawning with fork. */ +ProcHandle +do_spawn_fork (char *const args[], + char *workingDirectory, char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing) +{ + int forkCommunicationFds[2]; + int r = pipe(forkCommunicationFds); + if (r == -1) { + *failed_doing = "pipe"; + return -1; + } + + // Block signals with Haskell handlers. The danger here is that + // with the threaded RTS, a signal arrives in the child process, + // the RTS writes the signal information into the pipe (which is + // shared between parent and child), and the parent behaves as if + // the signal had been raised. + blockUserSignals(); + + // See #4074. Sometimes fork() gets interrupted by the timer + // signal and keeps restarting indefinitely. + stopTimer(); + + // N.B. execvpe is not supposed on some platforms. In this case + // we emulate this using fork and exec. However, to safely do so + // we need to perform all allocations *prior* to forking. Consequently, we + // need to find_executable before forking. +#if !defined(HAVE_execvpe) + char *exec_path; + if (environment) { + exec_path = find_executable(args[0]); + } +#endif + + int pid = myfork(); + switch(pid) + { + case -1: + unblockUserSignals(); + startTimer(); + close(forkCommunicationFds[0]); + close(forkCommunicationFds[1]); + *failed_doing = "fork"; + return -1; + + case 0: + // WARNING! We may now be in the child of vfork(), and any + // memory we modify below may also be seen in the parent + // process. + + close(forkCommunicationFds[0]); + fcntl(forkCommunicationFds[1], F_SETFD, FD_CLOEXEC); + + if ((flags & RUN_PROCESS_NEW_SESSION) != 0) { + setsid(); + } + if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) { + setpgid(0, 0); + } + + if (childGroup) { + if (setgid( *childGroup) != 0) { + // ERROR + child_failed(forkCommunicationFds[1], "setgid"); + } + } + + if (childUser) { + // Using setuid properly first requires that we initgroups. + // However, to do this we must know the username of the user we are + // switching to. + struct passwd pw; + struct passwd *res = NULL; + int buf_len = sysconf(_SC_GETPW_R_SIZE_MAX); + // TODO: Strictly speaking malloc is a no-no after fork() since it + // may try to take a lock + char *buf = malloc(buf_len); + gid_t suppl_gid = childGroup ? *childGroup : getgid(); + if ( getpwuid_r(*childUser, &pw, buf, buf_len, &res) != 0) { + child_failed(forkCommunicationFds[1], "getpwuid"); + } + if ( res == NULL ) { + child_failed(forkCommunicationFds[1], "getpwuid"); + } + if ( initgroups(res->pw_name, suppl_gid) != 0) { + child_failed(forkCommunicationFds[1], "initgroups"); + } + if ( setuid( *childUser) != 0) { + // ERROR + child_failed(forkCommunicationFds[1], "setuid"); + } + } + + unblockUserSignals(); + + if (workingDirectory) { + if (chdir (workingDirectory) < 0) { + child_failed(forkCommunicationFds[1], "chdir"); + } + } + + // Note [Ordering of handle closing] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Ordering matters here. If any of the FDs + // 0,1,2 were initially closed, then our pipes may have used + // these FDs. So when we dup2 the pipe FDs down to 0,1,2, we + // must do it in that order, otherwise we could overwrite an + // FD that we need later. See ticket #431. + + setup_std_handle_fork(STDIN_FILENO, stdInHdl, forkCommunicationFds[1]); + setup_std_handle_fork(STDOUT_FILENO, stdOutHdl, forkCommunicationFds[1]); + setup_std_handle_fork(STDERR_FILENO, stdErrHdl, forkCommunicationFds[1]); + + if ((flags & RUN_PROCESS_IN_CLOSE_FDS) != 0) { + int max_fd = get_max_fd(); + // XXX Not the pipe + for (int i = 3; i < max_fd; i++) { + if (i != forkCommunicationFds[1]) { + close(i); + } + } + } + + /* Reset the SIGINT/SIGQUIT signal handlers in the child, if requested + */ + if (flags & RESET_INT_QUIT_HANDLERS != 0) { + struct sigaction dfl; + (void)sigemptyset(&dfl.sa_mask); + dfl.sa_flags = 0; + dfl.sa_handler = SIG_DFL; + (void)sigaction(SIGINT, &dfl, NULL); + (void)sigaction(SIGQUIT, &dfl, NULL); + } + + /* the child */ + if (environment) { +#if defined(HAVE_execvpe) + // XXX Check result + execvpe(args[0], args, environment); +#else + // XXX Check result + execve(exec_path, args, environment); +#endif + } else { + // XXX Check result + execvp(args[0], args); + } + + child_failed(forkCommunicationFds[1], "exec"); + + default: + if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) { + setpgid(pid, pid); + } + close(forkCommunicationFds[1]); + fcntl(forkCommunicationFds[0], F_SETFD, FD_CLOEXEC); + + break; + } + + // If the child process had a problem, then it will tell us via the + // forkCommunicationFds pipe. First we try to read what the problem + // was. Note that if none of these conditionals match then we fall + // through and just return pid. + char *fail_reason; + r = read(forkCommunicationFds[0], &fail_reason, sizeof(fail_reason)); + if (r == -1) { + *failed_doing = "read pipe"; + pid = -1; + } + else if (r == sizeof(fail_reason)) { + *failed_doing = fail_reason; + + // Now we try to get the errno from the child + int err; + r = read(forkCommunicationFds[0], &err, sizeof(err)); + if (r == -1) { + *failed_doing = "read pipe"; + } else if (r != sizeof(err)) { + *failed_doing = "read pipe bad length"; + } else { + // If we succeed then we set errno. It'll be saved and + // restored again below. Note that in any other case we'll + // get the errno of whatever else went wrong instead. + errno = err; + } + + // We forked the child, but the child had a problem and stopped so it's + // our responsibility to reap here as nobody else can. + waitpid(pid, NULL, 0); + + // Already closed child ends above + if (stdInHdl->behavior == STD_HANDLE_USE_PIPE) { + close(stdInHdl->use_pipe.parent_end); + } + if (stdOutHdl->behavior == STD_HANDLE_USE_PIPE) { + close(stdOutHdl->use_pipe.parent_end); + } + if (stdErrHdl->behavior == STD_HANDLE_USE_PIPE) { + close(stdErrHdl->use_pipe.parent_end); + } + + pid = -1; + } + else if (r != 0) { + *failed_doing = "read pipe bad length"; + pid = -1; + } + + close(forkCommunicationFds[0]); + + unblockUserSignals(); + startTimer(); + + return pid; +} diff --git a/cbits/posix/posix_spawn.c b/cbits/posix/posix_spawn.c new file mode 100644 index 00000000..17d20435 --- /dev/null +++ b/cbits/posix/posix_spawn.c @@ -0,0 +1,211 @@ +#include "runProcess.h" +#include "common.h" + +#include +#include +#include + +#if !defined(HAVE_POSIX_SPAWNP) +ProcHandle +do_spawn_posix (char *const args[], + const char *workingDirectory, const char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing) +{ + return -2; +} + +#else + +// Necessary for POSIX_SPAWN_SETSID under glibc. +#define _GNU_SOURCE +#include + +extern char **environ; + +static int +setup_std_handle_spawn (int fd, + struct std_handle *hdl, + posix_spawn_file_actions_t *fa, + char **failed_doing) +{ + switch (hdl->behavior) { + case STD_HANDLE_CLOSE: + if (posix_spawn_file_actions_addclose(fa, fd) != 0) { + *failed_doing = "posix_spawn_file_actions_addclose"; + return -1; + } + return 0; + + case STD_HANDLE_USE_FD: + if (posix_spawn_file_actions_adddup2(fa, hdl->use_fd, fd) != 0) { + *failed_doing = "posix_spawn_file_actions_adddup2"; + return -1; + } + return 0; + + case STD_HANDLE_USE_PIPE: + if (hdl->use_pipe.child_end != fd) { + if (posix_spawn_file_actions_adddup2(fa, hdl->use_pipe.child_end, fd) != 0) { + *failed_doing = "posix_spawn_file_actions_adddup2(child_end)"; + return -1; + } + if (posix_spawn_file_actions_addclose(fa, hdl->use_pipe.child_end) != 0) { + *failed_doing = "posix_spawn_file_actions_addclose(child_end)"; + return -1; + } + } + if (posix_spawn_file_actions_addclose(fa, hdl->use_pipe.parent_end) != 0) { + *failed_doing = "posix_spawn_file_actions_addclose(parent_end)"; + return -1; + } + return 0; + } +} + +/* Try spawning with posix_spawn. Returns -1 if posix_spawn (or the particular + * requested configuration) is not supported. + */ +ProcHandle +do_spawn_posix (char *const args[], + char *workingDirectory, char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing) +{ + // First do catch some cases that posix_spawn simply can't handle... + if (childGroup || childUser) { + return -2; + } + if ((flags & RUN_PROCESS_IN_CLOSE_FDS) != 0) { + // TODO: can this be efficiently supported? + return -2; + } + + // Now the main act... + pid_t pid; + posix_spawn_file_actions_t fa; + posix_spawnattr_t sa; + int r; + ProcHandle ret; + short spawn_flags = 0; + + r = posix_spawn_file_actions_init(&fa); + if (r != 0) { + *failed_doing = "posix_spawn_file_actions_init"; + return -1; + } + + r = posix_spawnattr_init(&sa); + if (r != 0) { + posix_spawn_file_actions_destroy(&fa); + *failed_doing = "posix_spawnattr_init"; + return -1; + } + + if (workingDirectory) { +#if defined(HAVE_posix_spawn_file_actions_addchdir_np) + // N.B. this function is broken on macOS. + // See https://github.com/rust-lang/rust/pull/80537. + r = posix_spawn_file_actions_addchdir(&fa, workingDirectory); + if (r != 0) { + *failed_doing = "posix_spawn_file_actions_addchdir"; + goto fail; + } +#else + goto not_supported; +#endif + } + + if ((flags & RUN_PROCESS_NEW_SESSION) != 0) { +#if defined(HAVE_POSIX_SPAWN_SETSID) + // N.B. POSIX_SPAWN_SETSID is only supported on macOS 10.15 and later, + // but an proper error is not returned in earlier versions. + // See https://bugs.python.org/issue37586. + if (posix_spawnattr_setflags(&sa, POSIX_SPAWN_SETSID) != 0) { + *failed_doing = "posix_spawnattr_setflags(POSIX_SPAWN_SETSID)"; + goto fail; + } +#elif defined(HAVE_POSIX_SPAWN_SETSID_NP) + if (posix_spawnattr_setflags(&sa, POSIX_SPAWN_SETSID_NP) != 0) { + *failed_doing = "posix_spawnattr_setflags(POSIX_SPAWN_SETSID_NP)"; + goto fail; + } +#else + goto not_supported; +#endif + } + +#if defined(HAVE_POSIX_SPAWN_SETPGROUP) + if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) { + spawn_flags |= POSIX_SPAWN_SETPGROUP; + } +#endif + + if (setup_std_handle_spawn(STDIN_FILENO, stdInHdl, &fa, failed_doing) != 0) { + goto fail; + } + if (setup_std_handle_spawn(STDOUT_FILENO, stdOutHdl, &fa, failed_doing) != 0) { + goto fail; + } + if (setup_std_handle_spawn(STDERR_FILENO, stdErrHdl, &fa, failed_doing) != 0) { + goto fail; + } + + sigset_t ss; + if ((flags & RESET_INT_QUIT_HANDLERS) != 0) { + if (sigemptyset(&ss) == -1) { + *failed_doing = "sigemptyset"; + goto fail; + } + if (sigaddset(&ss, SIGINT) == -1) { + *failed_doing = "sigaddset(SIGINT)"; + goto fail; + } + if (sigaddset(&ss, SIGQUIT) == -1) { + *failed_doing = "sigaddset(SIGQUIT)"; + goto fail; + } + if (posix_spawnattr_setsigdefault(&sa, &ss) != 0) { + *failed_doing = "posix_spawnattr_setsigdefault"; + goto fail; + } + spawn_flags |= POSIX_SPAWN_SETSIGDEF; + } + + if (posix_spawnattr_setflags(&sa, spawn_flags) != 0) { + *failed_doing = "posix_spawnattr_setflags"; + goto fail; + } + + r = posix_spawnp(&pid, args[0], &fa, &sa, args, environment ? environment : environ); + if (r != 0) { + *failed_doing = "posix_spawnp"; + goto fail; + } else { + ret = pid; + goto finish; + } + +not_supported: + ret = -2; + goto finish; + +fail: + ret = -1; + goto finish; + +finish: + posix_spawn_file_actions_destroy(&fa); + posix_spawnattr_destroy(&sa); + return ret; +} + +#endif \ No newline at end of file diff --git a/cbits/posix/runProcess.c b/cbits/posix/runProcess.c index 7be513be..259ff7e0 100644 --- a/cbits/posix/runProcess.c +++ b/cbits/posix/runProcess.c @@ -4,408 +4,196 @@ Support for System.Process ------------------------------------------------------------------------- */ -/* XXX This is a nasty hack; should put everything necessary in this package */ -#include "HsBase.h" -#include "Rts.h" - #include "runProcess.h" +#include "common.h" -#include "execvpe.h" +#include +#include +#include -/* ---------------------------------------------------------------------------- - UNIX versions - ------------------------------------------------------------------------- */ +#ifdef HAVE_FCNTL_H +#include +#endif + +#if defined(HAVE_SIGNAL_H) +#include +#endif + +int +get_max_fd() +{ + static int cache = 0; + if (cache == 0) { +#if HAVE_SYSCONF + cache = sysconf(_SC_OPEN_MAX); + if (cache == -1) { + cache = 256; + } +#else + cache = 256; +#endif + } + return cache; +} // If a process was terminated by a signal, the exit status we return // via the System.Process API is (-signum). This encoding avoids collision with // normal process termination status codes. See also #7229. #define TERMSIG_EXITSTATUS(s) (-(WTERMSIG(s))) -static long max_fd = 0; - -// Rts internal API, not exposed in a public header file: -extern void blockUserSignals(void); -extern void unblockUserSignals(void); - -// These are arbitrarily chosen -- JP -#define forkSetgidFailed 124 -#define forkSetuidFailed 125 - -// See #1593. The convention for the exit code when -// exec() fails seems to be 127 (gleened from C's -// system()), but there's no equivalent convention for -// chdir(), so I'm picking 126 --SimonM. -#define forkChdirFailed 126 -#define forkExecFailed 127 +/* + * Spawn a new process. We first try posix_spawn but since this isn't supported + * on all platforms we fall back to fork/exec in some cases on some platforms. + */ +static ProcHandle +do_spawn (char *const args[], + char *workingDirectory, char **environment, + struct std_handle *stdInHdl, + struct std_handle *stdOutHdl, + struct std_handle *stdErrHdl, + gid_t *childGroup, uid_t *childUser, + int flags, + char **failed_doing) +{ + ProcHandle r; + r = do_spawn_posix(args, + workingDirectory, environment, + stdInHdl, stdOutHdl, stdErrHdl, + childGroup, childUser, + flags, + failed_doing); + if (r == -2) { + // configuration not supported by posix_spawn, fall back to fork/exec + } else { + return r; + } -#define forkGetpwuidFailed 128 -#define forkInitgroupsFailed 129 + r = do_spawn_fork(args, + workingDirectory, environment, + stdInHdl, stdOutHdl, stdErrHdl, + childGroup, childUser, + flags, + failed_doing); + return r; +} -__attribute__((__noreturn__)) -static void childFailed(int pipe, int failCode) { - int err; - ssize_t unused __attribute__((unused)); +enum pipe_direction { + CHILD_READS, CHILD_WRITES +}; + +/* Initialize a std_handle_behavior from a "pseudo-fd": + * - fd == -1 means create a pipe + * - fd == -2 means close the handle + * - otherwise use fd + * Returns 0 on success, -1 otherwise. + */ +static int +init_std_handle(int fd, enum pipe_direction direction, + /* out */ struct std_handle *hdl, + char **failed_doing) +{ + switch (fd) { + case -1: { + int pipe_fds[2]; + int r = pipe(pipe_fds); + if (r == -1) { + *failed_doing = "pipe"; + return -1; + } - err = errno; - unused = write(pipe, &failCode, sizeof(failCode)); - unused = write(pipe, &err, sizeof(err)); - // As a fallback, exit with the failCode - _exit(failCode); + int child_end = direction == CHILD_READS ? 0 : 1; + int parent_end = direction == CHILD_READS ? 1 : 0; + *hdl = (struct std_handle) { + .behavior = STD_HANDLE_USE_PIPE, + .use_pipe = { .child_end = pipe_fds[child_end], .parent_end = pipe_fds[parent_end] } + }; + break; + } + case -2: + *hdl = (struct std_handle) { + .behavior = STD_HANDLE_CLOSE + }; + break; + default: + *hdl = (struct std_handle) { + .behavior = STD_HANDLE_USE_FD, + .use_fd = fd + }; + break; + } + return 0; } ProcHandle runInteractiveProcess (char *const args[], char *workingDirectory, char **environment, + // handles to use for the standard handles. -1 indicates + // create pipe, -2 indicates close. int fdStdIn, int fdStdOut, int fdStdErr, + // output arguments to return created pipe handle to caller int *pfdStdInput, int *pfdStdOutput, int *pfdStdError, gid_t *childGroup, uid_t *childUser, int flags, char **failed_doing) { - int close_fds = ((flags & RUN_PROCESS_IN_CLOSE_FDS) != 0); - int pid; - int fdStdInput[2], fdStdOutput[2], fdStdError[2]; - int forkCommunicationFds[2]; - int r; - int failCode, err; - - // Ordering matters here, see below [Note #431]. - if (fdStdIn == -1) { - r = pipe(fdStdInput); - if (r == -1) { - *failed_doing = "runInteractiveProcess: pipe"; - return -1; - } - } - if (fdStdOut == -1) { - r = pipe(fdStdOutput); - if (r == -1) { - if (fdStdIn == -1) { - close(fdStdInput[0]); - close(fdStdInput[1]); - } - *failed_doing = "runInteractiveProcess: pipe"; - return -1; - } - } - if (fdStdErr == -1) { - r = pipe(fdStdError); - if (r == -1) { - *failed_doing = "runInteractiveProcess: pipe"; - if (fdStdIn == -1) { - close(fdStdInput[0]); - close(fdStdInput[1]); - } - if (fdStdOut == -1) { - close(fdStdOutput[0]); - close(fdStdOutput[1]); - } - return -1; - } - } + struct std_handle stdInHdl, stdOutHdl, stdErrHdl; + ProcHandle r; - r = pipe(forkCommunicationFds); - if (r == -1) { - *failed_doing = "runInteractiveProcess: pipe"; - if (fdStdIn == -1) { - close(fdStdInput[0]); - close(fdStdInput[1]); - } - if (fdStdOut == -1) { - close(fdStdOutput[0]); - close(fdStdOutput[1]); - } - if (fdStdErr == -1) { - close(fdStdError[0]); - close(fdStdError[1]); - } - return -1; - } - - // Block signals with Haskell handlers. The danger here is that - // with the threaded RTS, a signal arrives in the child process, - // the RTS writes the signal information into the pipe (which is - // shared between parent and child), and the parent behaves as if - // the signal had been raised. - blockUserSignals(); - - // See #4074. Sometimes fork() gets interrupted by the timer - // signal and keeps restarting indefinitely. - stopTimer(); - - switch(pid = myfork()) - { - case -1: - unblockUserSignals(); - startTimer(); - if (fdStdIn == -1) { - close(fdStdInput[0]); - close(fdStdInput[1]); - } - if (fdStdOut == -1) { - close(fdStdOutput[0]); - close(fdStdOutput[1]); - } - if (fdStdErr == -1) { - close(fdStdError[0]); - close(fdStdError[1]); - } - close(forkCommunicationFds[0]); - close(forkCommunicationFds[1]); - *failed_doing = "fork"; - return -1; - - case 0: - // WARNING! We may now be in the child of vfork(), and any - // memory we modify below may also be seen in the parent - // process. - - close(forkCommunicationFds[0]); - fcntl(forkCommunicationFds[1], F_SETFD, FD_CLOEXEC); - - if ((flags & RUN_PROCESS_NEW_SESSION) != 0) { - setsid(); - } - if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) { - setpgid(0, 0); - } + // A bit of paranoia to ensure that we catch if we fail to set this on + // failure. + *failed_doing = NULL; - if ( childGroup) { - if ( setgid( *childGroup) != 0) { - // ERROR - childFailed(forkCommunicationFds[1], forkSetgidFailed); - } - } - - if ( childUser) { - // Using setuid properly first requires that we initgroups. - // However, to do this we must know the username of the user we are - // switching to. - struct passwd pw; - struct passwd *res = NULL; - int buf_len = sysconf(_SC_GETPW_R_SIZE_MAX); - char *buf = malloc(buf_len); - gid_t suppl_gid = childGroup ? *childGroup : getgid(); - if ( getpwuid_r(*childUser, &pw, buf, buf_len, &res) != 0) { - childFailed(forkCommunicationFds[1], forkGetpwuidFailed); - } - if ( res == NULL ) { - childFailed(forkCommunicationFds[1], forkGetpwuidFailed); - } - if ( initgroups(res->pw_name, suppl_gid) != 0) { - childFailed(forkCommunicationFds[1], forkInitgroupsFailed); - } - if ( setuid( *childUser) != 0) { - // ERROR - childFailed(forkCommunicationFds[1], forkSetuidFailed); - } - } - - unblockUserSignals(); - - if (workingDirectory) { - if (chdir (workingDirectory) < 0) { - childFailed(forkCommunicationFds[1], forkChdirFailed); - } - } - - // [Note #431]: Ordering matters here. If any of the FDs - // 0,1,2 were initially closed, then our pipes may have used - // these FDs. So when we dup2 the pipe FDs down to 0,1,2, we - // must do it in that order, otherwise we could overwrite an - // FD that we need later. - - if (fdStdIn == -1) { - if (fdStdInput[0] != STDIN_FILENO) { - dup2 (fdStdInput[0], STDIN_FILENO); - close(fdStdInput[0]); - } - close(fdStdInput[1]); - } else if (fdStdIn == -2) { - close(STDIN_FILENO); - } else { - dup2(fdStdIn, STDIN_FILENO); - } - - if (fdStdOut == -1) { - if (fdStdOutput[1] != STDOUT_FILENO) { - dup2 (fdStdOutput[1], STDOUT_FILENO); - close(fdStdOutput[1]); - } - close(fdStdOutput[0]); - } else if (fdStdOut == -2) { - close(STDOUT_FILENO); - } else { - dup2(fdStdOut, STDOUT_FILENO); - } - - if (fdStdErr == -1) { - if (fdStdError[1] != STDERR_FILENO) { - dup2 (fdStdError[1], STDERR_FILENO); - close(fdStdError[1]); - } - close(fdStdError[0]); - } else if (fdStdErr == -2) { - close(STDERR_FILENO); - } else { - dup2(fdStdErr, STDERR_FILENO); - } - - if (close_fds) { - int i; - if (max_fd == 0) { -#if HAVE_SYSCONF - max_fd = sysconf(_SC_OPEN_MAX); - if (max_fd == -1) { - max_fd = 256; - } -#else - max_fd = 256; -#endif - } - // XXX Not the pipe - for (i = 3; i < max_fd; i++) { - if (i != forkCommunicationFds[1]) { - close(i); - } - } - } - - /* Reset the SIGINT/SIGQUIT signal handlers in the child, if requested - */ - if (flags & RESET_INT_QUIT_HANDLES != 0) { - struct sigaction dfl; - (void)sigemptyset(&dfl.sa_mask); - dfl.sa_flags = 0; - dfl.sa_handler = SIG_DFL; - (void)sigaction(SIGINT, &dfl, NULL); - (void)sigaction(SIGQUIT, &dfl, NULL); - } - - /* the child */ - if (environment) { - // XXX Check result - execvpe(args[0], args, environment); - } else { - // XXX Check result - execvp(args[0], args); - } - - childFailed(forkCommunicationFds[1], forkExecFailed); - - default: - if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) { - setpgid(pid, pid); - } - if (fdStdIn == -1) { - close(fdStdInput[0]); - fcntl(fdStdInput[1], F_SETFD, FD_CLOEXEC); - *pfdStdInput = fdStdInput[1]; - } - if (fdStdOut == -1) { - close(fdStdOutput[1]); - fcntl(fdStdOutput[0], F_SETFD, FD_CLOEXEC); - *pfdStdOutput = fdStdOutput[0]; - } - if (fdStdErr == -1) { - close(fdStdError[1]); - fcntl(fdStdError[0], F_SETFD, FD_CLOEXEC); - *pfdStdError = fdStdError[0]; - } - close(forkCommunicationFds[1]); - fcntl(forkCommunicationFds[0], F_SETFD, FD_CLOEXEC); - - break; + // Ordering matters here, see below Note [Ordering of handle closing]. + if (init_std_handle(fdStdIn, CHILD_READS, &stdInHdl, failed_doing) != 0) { + goto fail; } - // If the child process had a problem, then it will tell us via the - // forkCommunicationFds pipe. First we try to read what the problem - // was. Note that if none of these conditionals match then we fall - // through and just return pid. - r = read(forkCommunicationFds[0], &failCode, sizeof(failCode)); - if (r == -1) { - *failed_doing = "runInteractiveProcess: read pipe"; - pid = -1; + if (init_std_handle(fdStdOut, CHILD_WRITES, &stdOutHdl, failed_doing) != 0) { + goto fail; } - else if (r == sizeof(failCode)) { - // This is the case where we successfully managed to read - // the problem - switch (failCode) { - case forkChdirFailed: - *failed_doing = "runInteractiveProcess: chdir"; - break; - case forkExecFailed: - *failed_doing = "runInteractiveProcess: exec"; - break; - case forkSetgidFailed: - *failed_doing = "runInteractiveProcess: setgid"; - break; - case forkSetuidFailed: - *failed_doing = "runInteractiveProcess: setuid"; - break; - case forkGetpwuidFailed: - *failed_doing = "runInteractiveProcess: getpwuid"; - break; - case forkInitgroupsFailed: - *failed_doing = "runInteractiveProcess: initgroups"; - break; - default: - *failed_doing = "runInteractiveProcess: unknown"; - break; - } - // Now we try to get the errno from the child - r = read(forkCommunicationFds[0], &err, sizeof(err)); - if (r == -1) { - *failed_doing = "runInteractiveProcess: read pipe"; - } - else if (r != sizeof(failCode)) { - *failed_doing = "runInteractiveProcess: read pipe bad length"; - } - else { - // If we succeed then we set errno. It'll be saved and - // restored again below. Note that in any other case we'll - // get the errno of whatever else went wrong instead. - errno = err; - } - // We forked the child, but the child had a problem and stopped so it's - // our responsibility to reap here as nobody else can. - waitpid(pid, NULL, 0); - - if (fdStdIn == -1) { - // Already closed fdStdInput[0] above - close(fdStdInput[1]); - } - if (fdStdOut == -1) { - close(fdStdOutput[0]); - // Already closed fdStdOutput[1] above - } - if (fdStdErr == -1) { - close(fdStdError[0]); - // Already closed fdStdError[1] above - } - - pid = -1; + if (init_std_handle(fdStdErr, CHILD_WRITES, &stdErrHdl, failed_doing) != 0) { + goto fail; } - else if (r != 0) { - *failed_doing = "runInteractiveProcess: read pipe bad length"; - pid = -1; + + r = do_spawn(args, + workingDirectory, environment, + &stdInHdl, &stdOutHdl, &stdErrHdl, + childGroup, childUser, + flags, + failed_doing); + if (r == -1) { + goto fail; } - if (pid == -1) { - err = errno; + // Close the remote ends of any pipes we created. +#define FINALISE_STD_HANDLE(hdl, pfd) \ + if (hdl.behavior == STD_HANDLE_USE_PIPE) { \ + close(hdl.use_pipe.child_end); \ + fcntl(hdl.use_pipe.parent_end, F_SETFD, FD_CLOEXEC); \ + *pfd = hdl.use_pipe.parent_end; \ } - close(forkCommunicationFds[0]); + FINALISE_STD_HANDLE(stdInHdl, pfdStdInput); + FINALISE_STD_HANDLE(stdOutHdl, pfdStdOutput); + FINALISE_STD_HANDLE(stdErrHdl, pfdStdError); +#undef FINALISE_STD_HANDLE - unblockUserSignals(); - startTimer(); + return r; - if (pid == -1) { - errno = err; +fail: +#define CLOSE_PIPE(hdl) \ + if (hdl.behavior == STD_HANDLE_USE_PIPE) { \ + close(hdl.use_pipe.child_end); \ + close(hdl.use_pipe.parent_end); \ } - return pid; + CLOSE_PIPE(stdInHdl); + CLOSE_PIPE(stdOutHdl); + CLOSE_PIPE(stdErrHdl); +#undef CLOSE_PIPE + + return -1; } int @@ -421,14 +209,11 @@ getProcessExitCode (ProcHandle handle, int *pExitCode) *pExitCode = 0; - if ((res = waitpid(handle, &wstat, WNOHANG)) > 0) - { - if (WIFEXITED(wstat)) - { + if ((res = waitpid(handle, &wstat, WNOHANG)) > 0) { + if (WIFEXITED(wstat)) { *pExitCode = WEXITSTATUS(wstat); return 1; - } - else + } else { if (WIFSIGNALED(wstat)) { *pExitCode = TERMSIG_EXITSTATUS(wstat); @@ -438,6 +223,7 @@ getProcessExitCode (ProcHandle handle, int *pExitCode) { /* This should never happen */ } + } } if (res == 0) return 0; @@ -451,7 +237,8 @@ getProcessExitCode (ProcHandle handle, int *pExitCode) return -1; } -int waitForProcess (ProcHandle handle, int *pret) +int +waitForProcess (ProcHandle handle, int *pret) { int wstat; diff --git a/configure.ac b/configure.ac index 7599986a..115d4476 100644 --- a/configure.ac +++ b/configure.ac @@ -14,6 +14,13 @@ AC_FUNC_FORK AC_CHECK_HEADERS([signal.h sys/wait.h fcntl.h]) AC_CHECK_FUNCS([setitimer sysconf]) +AC_CHECK_FUNCS([execvpe]) + +# posix_spawn checks +AC_CHECK_HEADERS([spawn.h]) +AC_CHECK_FUNCS([posix_spawnp posix_spawn_file_actions_addchdir]) +AC_CHECK_DECLS([POSIX_SPAWN_SETSID, POSIX_SPAWN_SETSID_NP]) +AC_CHECK_DECLS([POSIX_SPAWN_SETPGROUP]) FP_CHECK_CONSTS([SIG_DFL SIG_IGN]) diff --git a/include/runProcess.h b/include/runProcess.h index 0ad05a65..7b80ddeb 100644 --- a/include/runProcess.h +++ b/include/runProcess.h @@ -4,6 +4,8 @@ Interface for code in runProcess.c (providing support for System.Process) ------------------------------------------------------------------------- */ +#pragma once + #include "HsProcessConfig.h" // Otherwise these clash with similar definitions from other packages: #undef PACKAGE_BUGREPORT @@ -19,35 +21,8 @@ #include #endif -#include -#include -#if !(defined(_MSC_VER) || defined(__MINGW32__) || defined(_WIN32)) -#include -#include -#endif - -#ifdef HAVE_FCNTL_H -#include -#endif - -#ifdef HAVE_VFORK_H -#include -#endif - -#if defined(HAVE_WORKING_VFORK) -#define myfork vfork -#elif defined(HAVE_WORKING_FORK) -#define myfork fork -// We don't need a fork command on Windows -#elif !(defined(_MSC_VER) || defined(__MINGW32__) || defined(_WIN32)) -#error Cannot find a working fork command -#endif - -#ifdef HAVE_SIGNAL_H -#include -#endif - #if !(defined(_MSC_VER) || defined(__MINGW32__) || defined(_WIN32)) +#include typedef pid_t ProcHandle; #else // Should really be intptr_t, but we don't have that type on the Haskell side @@ -58,6 +33,9 @@ typedef PHANDLE ProcHandle; #if !(defined(_MSC_VER) || defined(__MINGW32__) || defined(_WIN32)) +#include +#include + extern ProcHandle runInteractiveProcess( char *const args[], char *workingDirectory, char **environment, diff --git a/process.cabal b/process.cabal index 3adff85e..976db126 100644 --- a/process.cabal +++ b/process.cabal @@ -65,6 +65,9 @@ library else c-sources: cbits/posix/runProcess.c + cbits/posix/fork_exec.c + cbits/posix/posix_spawn.c + cbits/posix/find_executable.c other-modules: System.Process.Posix build-depends: unix >= 2.5 && < 2.8 diff --git a/tests/all.T b/tests/all.T index 33f4cdd3..2339d8c4 100644 --- a/tests/all.T +++ b/tests/all.T @@ -1,7 +1,11 @@ +# some platforms use spawnp instead of exec in some cases, resulting +# in spurious error output changes. +normalise_exec = normalise_fun(lambda s: s.replace('spawnp', 'exec')) + test('process001', extra_clean(['process001.out']), compile_and_run, ['']) test('process002', [fragile_for(16547, concurrent_ways), extra_clean(['process002.out'])], compile_and_run, ['']) test('process003', [fragile_for(17245, concurrent_ways), omit_ways(['ghci'])], compile_and_run, ['']) -test('process004', normalise_exe, compile_and_run, ['']) +test('process004', [normalise_exec, normalise_exe], compile_and_run, ['']) test('T1780', normal, compile_and_run, ['']) test('process005', omit_ways(['ghci']), compile_and_run, ['']) test('process006', normal, compile_and_run, ['']) @@ -34,7 +38,7 @@ test('T3994', [only_ways(['threaded1','threaded2']), test('T4889', normal, compile_and_run, ['']) test('process009', when(opsys('mingw32'), skip), compile_and_run, ['']) -test('process010', normal, compile_and_run, ['']) +test('process010', normalise_exec, compile_and_run, ['']) test('process011', when(opsys('mingw32'), skip), compile_and_run, ['']) test('T8343', normal, compile_and_run, ['']) diff --git a/tests/process004.stdout b/tests/process004.stdout index e6ca8c42..e8220702 100644 --- a/tests/process004.stdout +++ b/tests/process004.stdout @@ -1,2 +1,2 @@ -Exc: true: runInteractiveProcess: runInteractiveProcess: chdir: does not exist (No such file or directory) -Exc: true: runProcess: runInteractiveProcess: chdir: does not exist (No such file or directory) +Exc: true: runInteractiveProcess: chdir: invalid argument (Bad file descriptor) +Exc: true: runProcess: chdir: does not exist (No such file or directory) diff --git a/tests/process010.stdout b/tests/process010.stdout index 71b3f8e9..1c78052d 100644 --- a/tests/process010.stdout +++ b/tests/process010.stdout @@ -1,4 +1,4 @@ ExitSuccess ExitFailure 1 -Exc: /non/existent: rawSystem: runInteractiveProcess: exec: does not exist (No such file or directory) +Exc: /non/existent: rawSystem: posix_spawnp: illegal operation (Inappropriate ioctl for device) Done