From e0c6872cf40896c7be36b11dcc744620f10adf1d Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 2 Sep 2019 13:46:59 +0900 Subject: Initial commit --- dviware/dvi2bitmap/PipeStream.cc | 623 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 dviware/dvi2bitmap/PipeStream.cc (limited to 'dviware/dvi2bitmap/PipeStream.cc') diff --git a/dviware/dvi2bitmap/PipeStream.cc b/dviware/dvi2bitmap/PipeStream.cc new file mode 100644 index 0000000000..26502aa05a --- /dev/null +++ b/dviware/dvi2bitmap/PipeStream.cc @@ -0,0 +1,623 @@ +/* This file is part of dvi2bitmap; see README for copyrights and licence */ + +#include + +#include +#include + +#include + +#ifdef HAVE_CSTD_INCLUDE +# include +# include +# include +# include +# include +#else +# include +# include +# include +# include +# include +#endif + +// the assert headers appear to be slightly less predictable... +#if HAVE_CASSERT +# include +#elif HAVE_ASSERT_H +# include +#endif + +#include // this is standard according to single-unix, + // how POSIXy is that? How C++? + +/* + * Include , and don't bother with , which brings + * with it a number of problems related to declarations. Some of + * these are minor ones related to whether functions are or are not in + * std, but most are because only a subset of Unix/POSIX signal + * handling is part of the C++ standard, and we want to use more. + * This whole class is probably fairly Unix-specific, and this doesn't + * magically go away if we include . + * + * If it's available, explicitly include sys/signal.h as well as + * or . If we're compiling in a strict-ansi mode, + * the compiler may well have carefully avoided including these + * signals specific to Unix/POSIX, which are, of course, precisely the + * ones we're hoping to use. + * + * Include first: if includes it for us, + * then it will probably define sentinels which will stop us including + * it a second time. + */ +#if HAVE_SYS_SIGNAL_H +# include +#endif +#include + +#ifdef HAVE_SYS_WAIT_H +# include +#endif + +#ifdef HAVE_SYS_TIME_H +# include +#endif + +#include + +using STD::ostream; +using STD::ends; +using STD::endl; +using STD::cerr; + +#include + +#ifndef WIN32 +/** + * Takes care of the signal handling necessary for the functions in + * the {@link PipeStream} class. If your application for whatever + * reason has to catch SIGCHLD, then to avoid interference + * between these, you should do so using functions {@link + * #expectAnother} and {@link #got_status}. These two functions are the + * only `public' API in this namespace. + */ +namespace PipeStreamSignalHandling { + /* Handle SIGCHLDs by maintaining a set of pid/caught-status pairs. */ + struct process_status { + sig_atomic_t pid; + sig_atomic_t status; + process_status() : pid(0), status(-1) { } + void clear() { pid=0; status=-1; } + }; + struct process_status* procs = 0; // initial value triggers initialisation + sig_atomic_t nprocs; // max no of children we can wait for + sig_atomic_t nprocs_used; // number of slots used + bool got_status(pid_t pid, int* status); + void expectAnother() throw (InputByteStreamError); + extern "C" void childcatcher(int); + extern "C" void alarmcatcher(int); +} +#endif + +/** + * Creates a stream which returns the output of a given command. The + * constructor open a pipe with the given command, and prepares for + * the results to be read by this class's accessor methods. + * + *

If the second argument is omitted, then a default list of + * environment variables, namely PATH HOME LOGNAME SHELL TMPDIR, + * are inherited from the current environment. If present, it should + * be a space-separated list of environment variables with optional + * values: if no equal sign is present, the variable's value is + * inherited from the current environment. For example, if this list + * is given as in the form env1=val1 env2= env3, then + * env1 would receive the value val1, + * env2 would receive a null value, and env3 + * would be inherited. + * + *

If the command exits with a non-zero exit status, this is noted + * on stderr, but any output from the command is still returned. + * + *

If there's no way of doing this on a particular platform, throw + * InputByteStreamError always. + * + * @param cmd the command to be run + * + * @param envs if specified, this is a list of environment variables; + * if it is an empty string, "" (the default), then a + * default list of variables is inherited + * + * @throws InputByteStreamError on any errors +*/ +PipeStream::PipeStream (string cmd, string envs) + throw (InputByteStreamError) + : pipe_status_(-1), pid_(0), orig_command_(cmd) +{ +#ifdef HAVE_PIPE + /* + * Can't use popen, because we want to control the environment + * more. Initialisation of kpathsea adds settings to the + * environment which confuse an invocation of mktexpk. + * + * Implementation here follows useful example in + * + */ + PipeStreamSignalHandling::expectAnother(); + + int fd[2]; + + if (pipe(fd)) + throw new DviError("PipeStream: failed to create pipe"); + + pid_ = fork(); + if (pid_ < 0) { + throw new DviError("PipeStream: failed to fork"); + } else if (pid_ == 0) { + // ...in child. We will write to the pipe, so immediately + // close the reading end + ::close(fd[0]); + // And we want our stdout to go down the writing end + dup2(fd[1], STDOUT_FILENO); + ::close(fd[1]); + + char **argv = Util::string_list_to_array(Util::tokenise_string(cmd)); + + string defenvs = "PATH HOME LOGNAME SHELL TMPDIR"; + string_list envlist; + if (envs == "") { + envs = "+"; // triggers processing below + } + STD::map env; + for (string_list e = Util::tokenise_string(envs); + ! e.empty(); + e.pop_front()) { + if (e.front() == "+") { + e.pop_front(); // the "+" + string_list de = Util::tokenise_string(defenvs); + while (! de.empty()) { + e.push_front(de.front()); + de.pop_front(); + } + if (getVerbosity() > normal) { + cerr << "Added: now"; + for (string_list::const_iterator ci = e.begin(); + ci != e.end(); + ci++) { + cerr << " " << *ci; + } + cerr << endl; + } + } + string& s = e.front(); + size_t si = s.find('='); + if (si == string::npos) { + // no setting found + char *envval = getenv(s.c_str()); + if (envval != 0) { + //cerr << "set [" << s << "]=" << envval << endl; + env[s] = envval; + } + } else { + // value found + env[s.substr(0,si)] = s.substr(si+1); + //cerr << "substr: [" << s.substr(0,si) << "]=" << s.substr(si+1) << endl; + } + } + for (STD::map::const_iterator mi = env.begin(); + mi != env.end(); + mi++) { + string v = mi->first; + v.append("="); + v.append(mi->second); + envlist.push_back(v); + } + + if (getVerbosity() > normal) { + cerr << "Final envlist:"; + for (string_list::const_iterator si = envlist.begin(); + si != envlist.end(); + si++) { + cerr << " " << *si << endl; + } + } + + char **envp = Util::string_list_to_array(envlist); + + execve(argv[0], argv, envp); + + // This is an error + if (getVerbosity() > normal) + cerr << "Error executing " << argv[0] + << ": " << STD::strerror(errno) << endl; + + // Children should exit with _exit rather than exit. See, eg., + // + _exit(EXIT_FAILURE); + + } else { + + // ... in parent. Close the writing end. + ::close(fd[1]); + + bindToFileDescriptor(fd[0]); + + if (getVerbosity() > normal) + cerr << "PipeStream: pid=" << pid_ << endl; + } +#else /* HAVE_PIPE */ +#ifdef WIN32 + // On Windows (it seems) we _can_ use the popen function + if (getVerbosity() > normal) + cerr << "PipeStream: cmd = " << cmd << endl; + fp_ = popen(cmd.c_str(), "rb"); + if (fp_ == NULL){ + // This is an error + if (getVerbosity() > normal) + cerr << "Error executing " << cmd + << ": " << strerror(errno) << endl; + + throw new InputByteStreamError("popen failer"); + } + int fd = _fileno(fp_); + bindToFileDescriptor(fd); +#else // WIN32 + throw new InputByteStreamError("Have no pipe() function"); +#endif // WIN32 +#endif /* HAVE_PIPE */ + +} + +PipeStream::~PipeStream() +{ + close(); +} + +/** + * Closes the stream, and reaps the process status. + * + *

If the process has not already terminated, this method attempts + * to kill it, by sending first HUP then KILL. If + * there is some reason why we can't reap the status -- because it has + * not died or for, say, some permissions reason -- we set the + * returned status to -1. Any data not read from the pipe is discarded. + */ +void PipeStream::close(void) +{ + InputByteStream::close(); + +#ifdef WIN32 + pclose(fp_); + pipe_status_ = 0; +#else // WIN32 + if (pid_ > 0) { + // make sure that SIGALRM and SIGCHLD aren't blocked + sigset_t alrm_and_chld, oldmask; + sigemptyset(&alrm_and_chld); + sigaddset(&alrm_and_chld, SIGALRM); + sigaddset(&alrm_and_chld, SIGCHLD); + sigprocmask(SIG_UNBLOCK, &alrm_and_chld, &oldmask); + + int status = -1; + bool keep_waiting + = !PipeStreamSignalHandling::got_status(pid_, &status); + bool process_gone = false; + for (int i=0; keep_waiting; i++) { + + int sigtosend; + + if (process_gone) { + // We weren't able to signal to the process, but we still + // can't get its status. How has this happened? Give up. + keep_waiting = false; + } else { + switch (i) { + case 0: + sigtosend = 0; // check process (really, just wait) + break; + case 1: + sigtosend = SIGINT; + break; + case 2: + sigtosend = SIGKILL; + break; + default: + keep_waiting = false; // give up + break; + } + } + + if (keep_waiting) { + // temporarily block SIGALRM and SIGCHLD, until we can set + // an alarm for ourself + sigprocmask(SIG_BLOCK, &alrm_and_chld, 0); + if (kill(pid_, sigtosend) == 0) { + sigset_t zeromask; + sigemptyset(&zeromask); +#if defined(HAVE_SETITIMER) + struct itimerval timer; + timer.it_value.tv_sec = 1; + timer.it_value.tv_usec = 0; + timer.it_interval.tv_sec = 0; + timer.it_interval.tv_usec = 0; + setitimer(ITIMER_REAL, &timer, 0); +#elif defined(HAVE_ALARM) + alarm(1); +#else +#error "Have neither setitimer nor alarm. Can't proceed!" +#endif + sigsuspend(&zeromask); // unblock and pause + if (getVerbosity() > normal) + cerr << "PipeStream::close sent signal " << sigtosend + << " and waited for child" << endl; + } else { + // process wasn't there + process_gone = true; + if (getVerbosity() > normal) + cerr << "PipeStream::close: can't send signal " + << sigtosend << " to process " << pid_ + << " (" << STD::strerror(errno) << ")" + << endl; + } + + keep_waiting + = !PipeStreamSignalHandling::got_status(pid_, &status); + } + } + pipe_status_ = status; + pid_ = 0; + + // restore original signals mask + sigprocmask(SIG_SETMASK, &oldmask, 0); + } +#endif // WIN32 +} + +/** + * Returns the exit status of the command at the end of the pipe. Since + * this is only available after the process has completed, this + * invokes {@link #close} on the stream first. Thus you should not + * invoke this method until after you have extracted all of the + * command's output that you want. If close is unable + * to terminate the process for some reason, this returns -1. + * + *

If the process exited normally, return the exit status of the command, + * as opposed to the raw exit status returned from the process, since this is + * more useful than the raw status with assorted other status information + * concerning anomalous exits. This status is non-negative. If the process + * did not exit normally, then return the negative of the raw status + * information, which we assert will be less than zero. + * + *

See waitpid(2) for details. + * + * @return the exit status of the process. + */ +int PipeStream::getTerminationStatus(void) +{ + int retval; + + close(); + if (WIFEXITED(pipe_status_)) { + retval = WEXITSTATUS(pipe_status_); + assert(retval >= 0); + } else { + retval = -pipe_status_; + assert(retval < 0); // we are correct about this, aren't we? + } + return retval; +} + +/** + * Returns the contents of the stream as a string. If some of the + * stream has already been read by other methods of this class or its + * parent, it cannot be re-read. Any trailing whitespace, including + * end-of-line characters, is stripped. + * + * It's possible that the returned string will be incomplete, if the + * process failed somehow; irrespective of whether this happened, this + * returns what it can, and if this condition matters to you, you + * should check the termination status with + * getTerminationStatus. + * + * @param allOfFile if true, then all of the file is read into the + * string; if false (the default), only the first line is returned, + * not including the newline or carriage-return which ends + * the line + * + * @param gobbleRest if true (the default) then gobble the rest of + * the file; has an effect only if allOfFile was false + * + * @return the stream contents (or some of it, depending on the + * parameter values) as a string + * @throws InputByteStreamError if there is some problem reading the stream + */ + string PipeStream::getResult(bool allOfFile, bool gobbleRest) + throw (InputByteStreamError) +{ + SSTREAM resp; + if (getVerbosity() > normal) + cerr << "PipeStream::getResult(" << (allOfFile?"true":"false") + << ',' << (gobbleRest?"true":"false") << ")" << endl; + if (allOfFile) { + for (Byte b=getByte(); !eof(); b=getByte()) + resp << b; + } else { + for (Byte b=getByte(); !(eof() || b=='\n' || b=='\r'); b=getByte()) + resp << b; + if (gobbleRest) + while (!eof()) + getByte(); + } + string response = SS_STRING(resp); + if (getVerbosity() > normal) + cerr << "PipeStream::getResult: returning <" << response + << ">; eof=" << (eof() ? "true" : "false") << endl; + +// string response; +// int status = getTerminationStatus(); +// if (WIFEXITED(status)) { +// if (getVerbosity() > normal) +// cerr << "exit status=" << WEXITSTATUS(status) << endl; +// if (WEXITSTATUS(status) != 0 && getVerbosity() >= normal) { +// cerr << "Command <" << orig_command_ +// << "> exited with non-zero status " +// << WEXITSTATUS(status) << endl; +// } +// response = SS_STRING(resp); +// } else if (WIFSIGNALED(status)) { +// // if the command fails we may come here +// if (getVerbosity() >= normal) +// cerr << "Signalled with signal number " << WTERMSIG(status) +// << (WCOREDUMP(status) ? " (coredump)" : " (no coredump)") +// << endl; +// response = ""; +// } else if (WIFSTOPPED(status)) { +// if (getVerbosity() >= normal) +// cerr << "Stopped on signal " << WSTOPSIG(status) << endl; +// response = ""; +// } else { +// char msg[100]; +// sprintf(msg, "Impossible status %d from child %d", status, getPid()); +// throw InputByteStreamError(msg); +// } + + return response; +} + +#ifndef WIN32 +/* + * All this is a rather simple-minded version of the pattern described in + * + */ + +/** + * Note that we expect to start another child. This function must + * be called before doing each fork(), as it does the + * initialisation of the structures used by this set of functions, and + * subsequently ensures that there is enough space in the list of + * statuses to hold another one. + * + * @throws InputByteStreamError if we can't do this for some reason + */ +void PipeStreamSignalHandling::expectAnother() + throw (InputByteStreamError) +{ + sigset_t chld, oldmask; + sigemptyset(&chld); + sigaddset(&chld, SIGCHLD); + if (sigprocmask(SIG_BLOCK, &chld, &oldmask) < 0) + throw InputByteStreamError("Can't set signal mask"); + + if (procs == 0) { + // first time + nprocs = 8; + nprocs_used = 0; + procs = new struct process_status[nprocs]; + + struct sigaction sa_chld; + sa_chld.sa_handler = &PipeStreamSignalHandling::childcatcher; + sigemptyset(&sa_chld.sa_mask); +#ifdef SA_RESTART + sa_chld.sa_flags = SA_RESTART; +#else + sa_chld.sa_flags = 0; +#endif + if (sigaction(SIGCHLD, &sa_chld, 0) < 0) + throw new InputByteStreamError("Can't install CHLD handler"); + + struct sigaction sa_alrm; + sa_alrm.sa_handler = &PipeStreamSignalHandling::alarmcatcher; + sigemptyset(&sa_alrm.sa_mask); +#ifdef SA_INTERRUPT + sa_alrm.sa_flags = SA_INTERRUPT; // some SunOS +#else + sa_alrm.sa_flags = 0; +#endif + if (sigaction(SIGALRM, &sa_alrm, 0) < 0) + throw new InputByteStreamError("Can't install ALRM handler"); + + if (InputByteStream::getVerbosity() > normal) { + cerr << "expectAnother: initialised to " << nprocs << "; set now"; + for (int i=0; i= nprocs-1) { + assert(nprocs > 0); + int newnprocs = nprocs * 2; + struct process_status* newprocs = new struct process_status[nprocs]; + for (int i=0; i normal) { + cerr << "expectAnother: expanded to " << nprocs << "; set now"; + for (int i=0; istatus parameter, and return true. If not, return false; + * *status is then unchanged. + * + * @param pid the PID to check + * @param status a pointer to the status to be returned + * @return true if the status was in fact available, and is now in + * *status; false otherwise + */ +bool PipeStreamSignalHandling::got_status(pid_t pid, int* status) +{ + assert(status != 0); + for (int i=0; i(procs[i].status); + procs[i].clear(); + nprocs_used--; + if (InputByteStream::getVerbosity() > normal) + cerr << "got_status: pid=" << pid << " status=" << *status + << " (" + << i << "/" << nprocs_used << "/" << nprocs + << ")" << endl; + return true; + } + } + if (InputByteStream::getVerbosity() > normal) + cerr << "got_status: no status for pid " << pid << endl; + return false; +} + +void PipeStreamSignalHandling::childcatcher(int signum) +{ + assert(signum == SIGCHLD); + + int i; + int status; + pid_t caughtpid = waitpid(0, &status, 0); + for (i=0; i(caughtpid); + procs[i].status = static_cast(status); + nprocs_used++; + break; + } + } + // If i=nprocs, then there's no space to store this signal. + // Nothing much we can do -- but at least it isn't a zombie. + return; +} +void PipeStreamSignalHandling::alarmcatcher(int signum) +{ + assert(signum == SIGALRM); + return; // nothing to do +} + +#endif // WIN32 -- cgit v1.2.3