/************************************************************************* ** Process.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2021 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #ifdef _WIN32 #include "windows.hpp" #else #include #include #include #include #include #endif #include #include "FileSystem.hpp" #include "Process.hpp" #include "SignalHandler.hpp" using namespace std; /** Helper class that encapsulates the system-specific parts of * running a subprocess and retrieving its terminal output. */ class Subprocess { public: enum class State {RUNNING, FINISHED, FAILED}; public: Subprocess () =default; Subprocess (const Subprocess&) =delete; Subprocess (Subprocess&&) =delete; ~Subprocess (); bool run (const string &cmd, string params); bool readFromPipe (string &out); State state (); private: #ifdef _WIN32 HANDLE _pipeReadHandle=NULL; ///< handle of read end of pipe HANDLE _childProcHandle=NULL; ///< handle of child process #else int _readfd=-1; ///< file descriptor of read end of pipe pid_t _pid=-1; ///< PID of the subprocess #endif }; Process::Process (string cmd, string paramstr) : _cmd(std::move(cmd)), _paramstr(std::move(paramstr)) { } /** Runs the process and waits until it's finished. * @param[out] out takes the output written to stdout by the executed subprocess * @return true if process terminated properly * @throw SignalException if CTRL-C was pressed during execution */ bool Process::run (string *out) { Subprocess subprocess; if (!subprocess.run(_cmd, _paramstr)) return false; for (;;) { if (out) { out->clear(); subprocess.readFromPipe(*out); } Subprocess::State state = subprocess.state(); if (state != Subprocess::State::RUNNING) return state == Subprocess::State::FINISHED; SignalHandler::instance().check(); } } /** Runs the process in the given working directory and waits until it's finished. * @param[in] dir working directory * @param[out] out takes the output written to stdout by the executed process * @return true if process terminated properly * @throw SignalException if CTRL-C was pressed during execution */ bool Process::run (const string &dir, string *out) { bool ret = false; string cwd = FileSystem::getcwd(); if (FileSystem::chdir(dir)) { ret = run(out); ret &= FileSystem::chdir(cwd); } return ret; } // system-specific stuff #ifdef _WIN32 static inline void close_and_zero_handle (HANDLE &handle) { CloseHandle(handle); handle = NULL; } Subprocess::~Subprocess () { if (_pipeReadHandle != NULL) CloseHandle(_pipeReadHandle); if (_childProcHandle != NULL) { TerminateProcess(_childProcHandle, 1); CloseHandle(_childProcHandle); } } /** Retrieves output generated by child process. * @param[out] out read output is appended to this string * @returns false on errors */ bool Subprocess::readFromPipe (string &out) { if (!_pipeReadHandle) return false; bool success=false; bool processExited=false; DWORD len=0; while (PeekNamedPipe(_pipeReadHandle, NULL, 0, NULL, &len, NULL)) { // prevent blocking if (len == 0) { if (processExited) break; // process still busy processExited = (!_childProcHandle || WaitForSingleObject(_childProcHandle, 100) != WAIT_TIMEOUT); } else { char buf[4096]; success = ReadFile(_pipeReadHandle, buf, sizeof(buf), &len, NULL); if (!success || len == 0) break; out.append(buf, len); } } return success; } /** Starts a child process. * @param[in] cmd name of command to execute * @param[in] paramstr parameters required by command * @returns true if child process started properly */ bool Subprocess::run (const string &cmd, string paramstr) { SECURITY_ATTRIBUTES securityAttribs; ZeroMemory(&securityAttribs, sizeof(SECURITY_ATTRIBUTES)); securityAttribs.nLength = sizeof(SECURITY_ATTRIBUTES); securityAttribs.bInheritHandle = true; HANDLE pipeWriteHandle; // write end of pipe if (CreatePipe(&_pipeReadHandle, &pipeWriteHandle, &securityAttribs, 0) == ERROR_INVALID_HANDLE) return false; SetHandleInformation(_pipeReadHandle, HANDLE_FLAG_INHERIT, 0); HANDLE nullFile = CreateFile("NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &securityAttribs, OPEN_EXISTING, 0, NULL); bool success = false; if (nullFile != INVALID_HANDLE_VALUE) { STARTUPINFO startupInfo; ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); startupInfo.cb = sizeof(STARTUPINFO); startupInfo.dwFlags = STARTF_USESTDHANDLES; startupInfo.hStdInput = nullFile; startupInfo.hStdOutput = pipeWriteHandle; startupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION processInfo; ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION)); string cmdline = cmd + " " + paramstr; // put subprocess in separate process group to prevent its termination in case of CTRL-C success = CreateProcess(NULL, (LPSTR)cmdline.c_str(), NULL, NULL, true, CREATE_NEW_PROCESS_GROUP, NULL, NULL, &startupInfo, &processInfo); if (success) { _childProcHandle = processInfo.hProcess; CloseHandle(processInfo.hThread); } CloseHandle(nullFile); } CloseHandle(pipeWriteHandle); // must be closed before reading from pipe to prevent blocking if (!success) close_and_zero_handle(_pipeReadHandle); return success; } /** Returns the current state of the child process. */ Subprocess::State Subprocess::state () { DWORD status; if (!GetExitCodeProcess(_childProcHandle, &status)) return State::FAILED; if (status == STILL_ACTIVE) return State::RUNNING; close_and_zero_handle(_childProcHandle); return status == 0 ? State::FINISHED : State::FAILED; } #else // !_WIN32 Subprocess::~Subprocess () { if (_readfd >= 0) close(_readfd); if (_pid > 0) kill(_pid, SIGKILL); } /** Retrieves output generated by child process. * @param[out] out read output is appended to this string * @returns false on errors */ bool Subprocess::readFromPipe (string &out) { if (_readfd < 0 || _pid < 0) return false; char buf[1024]; ssize_t len; while ((len = read(_readfd, buf, sizeof(buf))) > 0) out.append(buf, len); if (len < 0) { close(_readfd); _readfd = -1; } return len >= 0; } /** Extracts whitespace-separated parameters from a string. * @param[in,out] paramstr the parameter string * @param[out] params vector holding pointers to the extracted parameters */ static void split_paramstr (string ¶mstr, vector ¶ms) { size_t left=0, right=0; // index of first and last character of current parameter char quote=0; // current quote character, 0=none const size_t len = paramstr.length(); while (left <= right && right < len) { while (left < len && isspace(paramstr[left])) ++left; if (left < len && (paramstr[left] == '"' || paramstr[left] == '\'')) quote = paramstr[left++]; right = left; while (right < len && (quote || !isspace(paramstr[right]))) { if (quote && paramstr[right] == quote) { quote=0; break; } else ++right; } if (right < len) paramstr[right]=0; if (left < len) params.push_back(¶mstr[left]); left = ++right; } } /** Starts a child process. * @param[in] cmd name of command to execute or absolute path to executable * @param[in] paramstr parameters required by the command * @returns true if child process started properly */ bool Subprocess::run (const string &cmd, string paramstr) { int pipefd[2]; if (cmd.empty() || pipe(pipefd) < 0) return false; _pid = fork(); if (_pid < 0) { close(pipefd[0]); close(pipefd[1]); return false; } if (_pid == 0) { // child process dup2(pipefd[1], STDOUT_FILENO); // redirect stdout to the pipe dup2(pipefd[1], STDERR_FILENO); // redirect stderr to the pipe close(pipefd[0]); close(pipefd[1]); vector params; params.push_back(cmd.c_str()); split_paramstr(paramstr, params); params.push_back(nullptr); // trailing null pointer marks end of parameter list signal(SIGINT, SIG_IGN); // child process is supposed to ignore ctrl-c events if (params[0][0] == '/') // absolute path to executable? params[0] = strrchr(params[0], '/')+1; // filename of executable execvp(cmd.c_str(), const_cast(params.data())); exit(1); } _readfd = pipefd[0]; close(pipefd[1]); // close write end of pipe return true; } /** Returns the current state of the child process. */ Subprocess::State Subprocess::state () { int status; pid_t wpid = waitpid(_pid, &status, WNOHANG); if (wpid == 0) return State::RUNNING; // still running _pid = -1; if (wpid > 0 && WIFEXITED(status) && WEXITSTATUS(status) == 0) return State::FINISHED; return State::FAILED; } #endif // !_WIN32