/**************************************************************************** ** ** This file is part of GAP, a system for computational discrete algebra. ** ** Copyright of GAP belongs to its developers, whose names are too numerous ** to list here. Please refer to the COPYRIGHT file for details. ** ** SPDX-License-Identifier: GPL-2.0-or-later ** ** ** This file will contains the functions for communicating with other ** processes via ptys and sockets ** ** The eventual intent is that there will be InputOutputStreams at the GAP ** level with some API to be defined, and two ways of creating them. One is ** like Process except that the external process is left running, the other ** connects as client to a specified socket. ** ** At this level, we provide the two interfaces separately. For each we have ** an integer identifier for each open connection, and creation, read and ** write functions, and possibly some sort of probe function. **
*/
#ifdef HAVE_OPENPTY #ifdefined(HAVE_UTIL_H) #include <util.h> // for openpty() on macOS, OpenBSD and NetBSD #elifdefined(HAVE_LIBUTIL_H) #include <libutil.h> // for openpty() on FreeBSD #elifdefined(HAVE_PTY_H) #include <pty.h> // for openpty() on Cygwin, Interix, OSF/1 4 and 5 #endif #endif
// LOCKING // In HPC-GAP, be sure to HashLock PtyIOStreams before accessing any of // the IOStream related variables, including FreeptyIOStreams
typedefstruct {
pid_t childPID; // Also used as a link to make a linked free list int ptyFD; // GAP reading from external prog int inuse; /* we need to scan all the "live" structures when we have had SIGCHLD so, for now, we just walk the array for
the ones marked in use */ int changed; /* set non-zero by the signal handler if our child has
done something -- stopped or exited */ int status; // status from wait3 -- meaningful only if changed is 1 int blocked; // we have already reported a problem, which is still there int alive; /* gets set after waiting for a child actually fails
implying that the child has vanished under our noses */
} PtyIOStream;
enum { // maximal number of pseudo ttys we will allocate
MAX_PTYS = 64,
// maximal length of argument string for CREATE_PTY_IOSTREAM
MAX_ARGS = 1000
};
static PtyIOStream PtyIOStreams[MAX_PTYS];
// FreePtyIOStreams is the index of the first unused slot of the PtyIOStreams // array, or else -1 if there is none. The childPID field of each free slot in // turn is set to the position of the next free slot (or -1), so that the free // slots form a linked list. staticInt FreePtyIOStreams;
/**************************************************************************** ** *F OpenPty( <parent>, <child> ) . . . . . . . . open a pseudo terminal
*/
#ifdef HAVE_OPENPTY
static UInt OpenPty(int * parent, int * child)
{ /* openpty is available on OpenBSD, NetBSD and FreeBSD, macOS, Cygwin, Interix, OSF/1 4 and 5, and glibc (since 1998), and hence on most modern Linux systems. See also: https://www.gnu.org/software/gnulib/manual/html_node/openpty.html */ return (openpty(parent, child, NULL, NULL, NULL) < 0);
}
#elifdefined(HAVE_POSIX_OPENPT)
static UInt OpenPty(int * parent, int * child)
{ /* Attempt to use POSIX 98 pseudo ttys. Opening a parent tty is done via posix_openpt, which is available on virtually every current UNIX system; indeed, according to gnulib, it is available on at least the following systems: - glibc >= 2.2.1 (released January 2001; but is a stub on GNU/Hurd), - macOS >= 10.4 (released April 2005), - FreeBSD >= 5.1 (released June 2003), - NetBSD >= 3.0 (released December 2005), - AIX >= 5.2 (released October 2002), - HP-UX >= 11.31 (released February 2007), - Solaris >= 10 (released January 2005), - Cygwin >= 1.7 (released December 2009). Systems lacking posix_openpt (in addition to older versions of the systems listed above) include: - OpenBSD - Minix 3.1.8 - IRIX 6.5 - OSF/1 5.1 - mingw - MSVC 9 - Interix 3.5 - BeOS
*/
*parent = posix_openpt(O_RDWR | O_NOCTTY); if (*parent < 0) {
PErr("OpenPty: posix_openpt failed"); return 1;
}
if (grantpt(*parent)) {
PErr("OpenPty: grantpt failed"); goto error;
} if (unlockpt(*parent)) {
close(*parent);
PErr("OpenPty: unlockpt failed"); goto error;
}
static UInt OpenPty(int * parent, int * child)
{
Pr("no pseudo tty support available\n", 0, 0); return 1;
}
#endif
/**************************************************************************** ** *F StartChildProcess( <dir>, <name>, <args> ) ** Start a subprocess using ptys. Returns the stream number of the IOStream ** that is connected to the new process
*/
// Clean up a signalled or exited child process // CheckChildStatusChanged must be called by libraries which replace GAP's // signal handler, or call 'waitpid'. // The function should be passed a PID, and the return value of waitpid. // Returns 1 if that PID was a child owned by GAP, or 0 otherwise. int CheckChildStatusChanged(int childPID, int status)
{
GAP_ASSERT(childPID > 0);
GAP_ASSERT((WIFEXITED(status) || WIFSIGNALED(status)));
HashLock(PtyIOStreams); for (UInt i = 0; i < MAX_PTYS; i++) { if (PtyIOStreams[i].inuse && PtyIOStreams[i].childPID == childPID) {
PtyIOStreams[i].changed = 1;
PtyIOStreams[i].status = status;
PtyIOStreams[i].blocked = 0;
HashUnlock(PtyIOStreams); return 1;
}
}
HashUnlock(PtyIOStreams); return 0;
}
staticvoid ChildStatusChanged(int whichsig)
{
UInt i; int status; int retcode;
assert(whichsig == SIGCHLD);
HashLock(PtyIOStreams); for (i = 0; i < MAX_PTYS; i++) { if (PtyIOStreams[i].inuse) {
retcode = waitpid(PtyIOStreams[i].childPID, &status,
WNOHANG | WUNTRACED); if (retcode != -1 && retcode != 0 &&
(WIFEXITED(status) || WIFSIGNALED(status))) {
PtyIOStreams[i].changed = 1;
PtyIOStreams[i].status = status;
PtyIOStreams[i].blocked = 0;
}
}
}
HashUnlock(PtyIOStreams);
// // posix_spawn_file_actions_addchdir was only recently added to POSIX, and // most implementations therefore do not yet use this final name, but instead // provide the functionality under the alternate name // posix_spawn_file_actions_addchdir_np. To keep things simple, we detect this // case and use some preprocessor tricks to make things work in either case. // // macOS 26 has posix_spawn_file_actions_addchdir, but Xcode generates // code which uses it even in older versions of macOS. Therefore if both // functions are defined, we will always use the older _np version. // See issue <https://github.com/gap-system/gap/issues/6118> for details. #ifdefined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) || \ defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)
// Disable posix_spawn in HPC-GAP if we can't use it in a thread-safe manner #ifdefined(HPCGAP) && !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) #undef HAVE_POSIX_SPAWN #endif
// if neither posix_spawn_file_actions_addchdir nor O_CLOEXEC are available, // our posix_spawn code should not be used. This affects e.g. Linux systems // with an old glibc, see https://github.com/gap-system/gap/issues/3918. #if !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) && !defined(O_CLOEXEC) #undef HAVE_POSIX_SPAWN #endif
#ifdef HAVE_POSIX_SPAWN
// O_DIRECTORY is not available in old Linux / glibc versions, but the way we // use it below, it is optional anyway #ifndef O_DIRECTORY #define O_DIRECTORY 0 #endif
// The following function behaves like posix_spawn, but also // changes the working directory temporarily to 'dir'. staticint posix_spawn_with_dir(pid_t * pid, constchar * path,
posix_spawn_file_actions_t * file_actions, const posix_spawnattr_t * attrp, char * const argv[], char * const envp[], constchar * dir)
{ // For systems that don't support fork+exec well (e.g. embedded systems, // but also Windows), the POSIX Issue 6 (~2001) added posix_spawn() and // friends. These can be used as a replacement for fork+exec in many // applications. However, they curiously miss one very common demand when // launching subprocesses: there is no way to override the working // directory for the child process. This well-known deficiency has finally // been realized as a problem by POSIX in 2018, just about 17 years after // posix_spawn was first put into the standard, and so we might see a // proper fix for this soon, i.e., possibly even within the next decade! // See also <http://austingroupbugs.net/view.php?id=1208>. // // UPDATE: musl libc 1.1.24, glibc 2.29 and macOS 10.15 added preview // versions of these new APIs (with suffix `_np` to indicate it), so we // can actually start using them. // // UPDATE: the proposed change was accepted and applied to the draft of // the next POSIX revision in mid-2020. When this will appear in public // is anyones guess. On the upside, also OpenBSD, FreeBSD, and Solaris // implement the _np versions of the API. // // UPDATE: For now we still prefer the _np version if available, to // avoid issues in macOS 26 (see <https://github.com/gap-system/gap/issues/6118>.
#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR if (posix_spawn_file_actions_addchdir_func(file_actions, dir)) {
PErr("posix_spawn_with_dir: addchdir failed"); return 1;
} #else // For older systems that lack posix_spawn_file_actions_addchdir or // posix_spawn_file_actions_addchdir_np, we use the following fallback // code. // // WARNING: This is not thread safe! As explained above, until recently, // there was no portable way to do this race free, without using an // external shim executable which sets the wd and then calls the actually // target executable. int oldwd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (oldwd == -1) {
PErr("posix_spawn_with_dir: cannot open current working directory"); return 1;
} if (chdir(dir) == -1) {
PErr("posix_spawn_with_dir: cannot change working " "directory for subprocess"); return 1;
} #endif
// spawn subprocess int res = posix_spawn(pid, path, file_actions, attrp, argv, envp); if (res) {
PErr("StartChildProcess: posix_spawn failed");
}
#ifndef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR // restore working directory if (fchdir(oldwd)) {
PErr("StartChildProcess: failed to restore working dir");
}
close(oldwd); // ignore error #endif return res;
}
#endif
staticInt
StartChildProcess(constChar * dir, constChar * prg, Char * args[])
{ int child; // pipe to child Int stream;
struct termios tst; // old and new terminal state
HashLock(PtyIOStreams);
// Get a stream record
stream = NewStream(); if (stream == -1) {
HashUnlock(PtyIOStreams); return -1;
}
// open pseudo terminal for communication with gap if (OpenPty(&PtyIOStreams[stream].ptyFD, &child)) {
PErr("StartChildProcess: open pseudo tty failed");
FreeStream(stream);
HashUnlock(PtyIOStreams); return -1;
}
// Now fiddle with the terminal sessions on the pty if (tcgetattr(child, &tst) == -1) {
PErr("StartChildProcess: tcgetattr on child pty failed"); goto cleanup;
}
tst.c_cc[VINTR] = 0377;
tst.c_cc[VQUIT] = 0377;
tst.c_iflag &= ~(INLCR|ICRNL);
tst.c_cc[VMIN] = 1;
tst.c_cc[VTIME] = 0;
tst.c_lflag &= ~(ECHO|ICANON);
tst.c_oflag &= ~(ONLCR); if (tcsetattr(child, TCSANOW, &tst) == -1) {
PErr("StartChildProcess: tcsetattr on child pty failed"); goto cleanup;
}
// set input to non blocking operation // Not any more
// cleanup if (posix_spawn_file_actions_destroy(&file_actions)) {
PErr("StartChildProcess: posix_spawn_file_actions_destroy failed"); goto cleanup;
} #else
PtyIOStreams[stream].childPID = fork(); if (PtyIOStreams[stream].childPID == 0) { // Set up the child
close(PtyIOStreams[stream].ptyFD); if (dup2(child, 0) == -1)
_exit(-1);
fcntl(0, F_SETFD, 0);
if (dup2(child, 1) == -1)
_exit(-1);
fcntl(1, F_SETFD, 0);
if (chdir(dir) == -1) {
_exit(-1);
}
#ifdef HAVE_SETPGID
setpgid(0, 0); #endif
execv(prg, args);
// This should never happen
close(child);
_exit(1);
} #endif
// Now we're back in the parent // check if the fork was successful if (PtyIOStreams[stream].childPID == -1) {
PErr("StartChildProcess: cannot fork to subprocess"); goto cleanup;
}
close(child);
/**************************************************************************** ** *F SyExecuteProcess( <dir>, <prg>, <in>, <out>, <args> ) . . . . new process ** ** Start <prg> in directory <dir> with standard input connected to <in>, ** standard output connected to <out> and arguments. No path search is ** performed, the return value of the process is returned if the operation ** system supports such a concept.
*/
static UInt
SyExecuteProcess(Char * dir, Char * prg, Int in, Int out, Char * args[])
{ int savestdin, savestdout; Int tin, tout; int res;
// change the working directory if (chdir(dir) == -1) return -1;
// if <in> is -1 open "/dev/null" if (in == -1)
tin = open("/dev/null", O_RDONLY); else
tin = SyBufFileno(in); if (tin == -1) return -1;
// if <out> is -1 open "/dev/null" if (out == -1)
tout = open("/dev/null", O_WRONLY); else
tout = SyBufFileno(out); if (tout == -1) { if (in == -1)
close(tin); return -1;
}
// set standard input to <in>, standard output to <out>
savestdin = -1; // Just to please the compiler if (tin != 0) {
savestdin = dup(0); if (savestdin == -1 || dup2(tin, 0) == -1) { if (out == -1)
close(tout); if (in == -1)
close(tin); return -1;
}
fcntl(0, F_SETFD, 0);
}
if (tout != 1) {
savestdout = dup(1); if (savestdout == -1 || dup2(tout, 1) == -1) { if (tin != 0) {
close(0);
dup2(savestdin, 0);
close(savestdin);
} if (out == -1)
close(tout); if (in == -1)
close(tin); return -1;
}
fcntl(1, F_SETFD, 0);
}
FreezeStdin = 1; // now try to execute the program
res = spawnve(_P_WAIT, prg, (constchar * const *)args,
(constchar * const *)environ);
// Now repair the open file descriptors: if (tout != 1) {
close(1);
dup2(savestdout, 1);
close(savestdout);
} if (tin != 0) {
close(0);
dup2(savestdin, 0);
close(savestdin);
} if (out == -1)
close(tout); if (in == -1)
close(tin);
static UInt
SyExecuteProcess(Char * dir, Char * prg, Int in, Int out, Char * args[])
{
pid_t pid; // process id
pid_t wait_pid; int status; // do not use `Int' Int tin; // temp in Int tout; // temp out
sig_handler_t * volatile func2;
// turn off the SIGCHLD handling, so that we can be sure to collect this // child `After that, we call the old signal handler, in case any other // children have died in the meantime. This resets the handler
func2 = signal(SIGCHLD, SIG_DFL);
// This may return SIG_DFL (0x0) or SIG_IGN (0x1) if the previous handler // was set to the default or 'ignore'. In these cases (or if SIG_ERR is // returned), just use a null signal handler - the default on most systems // is to do nothing if (func2 == SIG_ERR || func2 == SIG_DFL || func2 == SIG_IGN)
func2 = &NullSignalHandler;
// clone the process
pid = fork(); if (pid == -1) { return -1;
}
// we are the parent if (pid != 0) { // Stop trying to read input
FreezeStdin = 1;
// ignore a CTRL-C struct sigaction sa; struct sigaction oldsa;
// wait for some action
wait_pid = waitpid(pid, &status, 0);
FreezeStdin = 0;
sigaction(SIGINT, &oldsa, NULL);
(*func2)(SIGCHLD); if (wait_pid == -1) { return -1;
} if (WIFSIGNALED(status)) { return -1;
} return WEXITSTATUS(status);
}
// we are the child else {
// change the working directory if (chdir(dir) == -1) {
_exit(-1);
}
// if <in> is -1 open "/dev/null" if (in == -1) {
tin = open("/dev/null", O_RDONLY);
} else {
tin = SyBufFileno(in);
} if (tin == -1) {
_exit(-1);
}
// if <out> is -1 open "/dev/null" if (out == -1) {
tout = open("/dev/null", O_WRONLY);
} else {
tout = SyBufFileno(out);
} if (tout == -1) {
_exit(-1);
}
// set standard input to <in>, standard output to <out> if (tin != 0) { if (dup2(tin, 0) == -1) {
_exit(-1);
}
}
fcntl(0, F_SETFD, 0);
if (tout != 1) { if (dup2(tout, 1) == -1) {
_exit(-1);
}
}
fcntl(1, F_SETFD, 0);
// now try to execute the program
execve(prg, args, environ);
_exit(-1);
}
// this should not happen return -1;
} #endif
#endif
#endif// !GAP_DISABLE_SUBPROCESS_CODE
// This function assumes that the caller invoked HashLock(PtyIOStreams). // It unlocks just before throwing any error. staticvoid HandleChildStatusChanges(UInt pty)
{ // common error handling, when we are asked to read or write to a stopped // or dead child if (PtyIOStreams[pty].alive == 0) {
PtyIOStreams[pty].changed = 0;
PtyIOStreams[pty].blocked = 0;
HashUnlock(PtyIOStreams);
ErrorQuit("Child Process is unexpectedly dead", 0, 0);
} elseif (PtyIOStreams[pty].blocked) {
HashUnlock(PtyIOStreams);
ErrorQuit("Child Process is still dead", 0, 0);
} elseif (PtyIOStreams[pty].changed) {
PtyIOStreams[pty].blocked = 1;
PtyIOStreams[pty].changed = 0; Int cPID = PtyIOStreams[pty].childPID; Int status = PtyIOStreams[pty].status;
HashUnlock(PtyIOStreams);
ErrorQuit("Child Process %d has stopped or died, status %d", cPID,
status);
}
}
static Obj FuncCREATE_PTY_IOSTREAM(Obj self, Obj dir, Obj prog, Obj args)
{
Obj allargs[MAX_ARGS + 1]; Char * argv[MAX_ARGS + 2];
UInt i, len; Int pty;
len = LEN_LIST(args); if (len > MAX_ARGS)
ErrorQuit("Too many arguments", 0, 0);
ConvString(dir);
ConvString(prog); for (i = 1; i <= len; i++) {
allargs[i] = ELM_LIST(args, i);
ConvString(allargs[i]);
} // From here we cannot afford to have a garbage collection
argv[0] = CSTR_STRING(prog); for (i = 1; i <= len; i++) {
argv[i] = CSTR_STRING(allargs[i]);
}
argv[i] = (Char *)0;
pty = StartChildProcess(CONST_CSTR_STRING(dir), CONST_CSTR_STRING(prog),
argv); if (pty < 0) return Fail; else return ObjInt_Int(pty);
}
staticInt ReadFromPty2(UInt stream, Char * buf, Int maxlen, UInt block)
{ // read at most maxlen bytes from stream, into buf. If block is non-zero // then wait for at least one byte to be available. Otherwise don't. // Return the number of bytes read, or -1 for error. A blocking return // having read zero bytes definitely indicates an end of file
Int nread = 0; int ret;
while (maxlen > 0) { #ifdef HAVE_SELECT if (!block || nread > 0) {
fd_set set; struct timeval tv; do {
FD_ZERO(&set);
FD_SET(PtyIOStreams[stream].ptyFD, &set);
tv.tv_sec = 0;
tv.tv_usec = 0;
ret = select(PtyIOStreams[stream].ptyFD + 1, &set, NULL, NULL,
&tv);
} while (ret == -1 && errno == EAGAIN); if (ret == -1 && nread == 0) return -1; if (ret < 1) return nread ? nread : -1;
} #endif do {
ret = read(PtyIOStreams[stream].ptyFD, buf, maxlen);
} while (ret == -1 && errno == EAGAIN); if (ret == -1 && nread == 0) return -1; if (ret < 1) return nread;
nread += ret;
buf += ret;
maxlen -= ret;
} return nread;
}
static UInt WriteToPty(UInt stream, Char * buf, Int len)
{ Int res; Int old; if (len < 0) { // FIXME: why allow 'len' to be negative here? To allow // invoking a "raw" version of `write` perhaps? But we don't // seem to use that anywhere. So perhaps get rid of it or // even turn it into an error?! return write(PtyIOStreams[stream].ptyFD, buf, -len);
}
old = len; while (0 < len) {
res = write(PtyIOStreams[stream].ptyFD, buf, len); if (res < 0) {
HandleChildStatusChanges(stream); if (errno == EAGAIN) { continue;
} else // FIXME: by returning errno, we make it impossible for the // caller to detect errors. return errno;
}
len -= res;
buf += res;
} return old;
}
static UInt HashLockStreamIfAvailable(Obj stream)
{
UInt pty = INT_INTOBJ(stream);
HashLock(PtyIOStreams); if (!PtyIOStreams[pty].inuse) {
HashUnlock(PtyIOStreams);
ErrorMayQuit("IOSTREAM %d is not in use", pty, 0);
} return pty;
}
// Close down the child
kill(PtyIOStreams[pty].childPID, SIGTERM);
// GAP (or another library) might wait on this PID before // we handle it. If that happens, waitpid will return -1.
retcode = waitpid(PtyIOStreams[pty].childPID, &status, WNOHANG); if (retcode == 0) { // Give process a second to quit
sleep(1);
retcode = waitpid(PtyIOStreams[pty].childPID, &status, WNOHANG);
} if (retcode == 0) { // Hard kill process
kill(PtyIOStreams[pty].childPID, SIGKILL);
retcode = waitpid(PtyIOStreams[pty].childPID, &status, 0);
}
retcode = close(PtyIOStreams[pty].ptyFD); if (retcode)
Pr("Strange close return code %d\n", retcode, 0);
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.22Bemerkung:
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.