From 693d22a89693f93b0bc146d76459d4d79c3b6e68 Mon Sep 17 00:00:00 2001 From: Jean-Francois Dockes Date: Tue, 8 Sep 2015 10:24:19 +0200 Subject: [PATCH] first non-compiling and incomplete draft of the windows execmd module --HG-- branch : WINDOWSPORT --- src/utils/execmd.cpp | 2 +- src/utils/execmd.h | 3 +- src/windows/execmd_w.cpp | 491 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 src/windows/execmd_w.cpp diff --git a/src/utils/execmd.cpp b/src/utils/execmd.cpp index 356a683e..d464ab39 100644 --- a/src/utils/execmd.cpp +++ b/src/utils/execmd.cpp @@ -15,7 +15,7 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #ifndef TEST_EXECMD -#ifdef RECOLL_DATADIR +#ifdef BUILDING_RECOLL #include "autoconfig.h" #else #include "config.h" diff --git a/src/utils/execmd.h b/src/utils/execmd.h index 6b548537..1dbae326 100644 --- a/src/utils/execmd.h +++ b/src/utils/execmd.h @@ -16,7 +16,6 @@ */ #ifndef _EXECMD_H_INCLUDED_ #define _EXECMD_H_INCLUDED_ -#ifndef _WIN32 #include #include @@ -247,5 +246,5 @@ private: std::string m_reason; std::stack m_atexitfuncs; }; -#endif /* !_WIN32 */ + #endif /* _EXECMD_H_INCLUDED_ */ diff --git a/src/windows/execmd_w.cpp b/src/windows/execmd_w.cpp new file mode 100644 index 00000000..eb9fc55d --- /dev/null +++ b/src/windows/execmd_w.cpp @@ -0,0 +1,491 @@ + +#include "execmd.h" + +#include +#include +#include + +using namespace std; + +class ExecCmd::Internal { +public: + Internal() + : m_advise(0), m_provide(0), m_timeoutMs(1000), + m_pid(-1), m_hOutputRead(NULL), m_hInputWrite(NULL) { + } + + std::vector m_env; + ExecCmdAdvise *m_advise; + ExecCmdProvide *m_provide; + bool m_killRequest; + int m_timeoutMs; + string m_stderrFile; + // Subprocess id + pid_t m_pid; + HANDLE m_hOutputRead; + HANDLE m_hInputWrite; + OVERLAPPED m_oOutputRead; // Do these need resource control? + OVERLAPPED m_oInputWrite; + PROCESS_INFORMATION m_piProcInfo; + + // Reset internal state indicators. Any resources should have been + // previously freed + void reset() { + m_killRequest = false; + m_hOutputRead = NULL; + m_hInputWrite = NULL; + memset(&m_oOutputRead, 0, sizeof(m_oOutputRead)); + memset(&m_oInputWrite, 0, sizeof(m_oInputWrite)); + m_pid = -1; + } +}; + +ExecCmd::ExecCmd() +{ + m = new Internal(); + if (m) { + m->reset(); + } +} + +void ExecCmd::setAdvise(ExecCmdAdvise *adv) +{ + m->m_advise = adv; +} +void ExecCmd::setProvide(ExecCmdProvide *p) +{ + m->m_provide = p; +} +void ExecCmd::setTimeout(int mS) +{ + if (mS > 30) { + m->m_timeoutMs = mS; + } +} +void ExecCmd::setStderr(const std::string& stderrFile) +{ + m->m_stderrFile = stderrFile; +} +pid_t ExecCmd::getChildPid() +{ + return m->m_pid; +} +void ExecCmd::setKill() +{ + m->m_killRequest = true; +} +void ExecCmd::zapChild() +{ + setKill(); + (void)wait(); +} + + +bool ExecCmd::Internal::PreparePipes(bool has_input,HANDLE *hChildInput, + bool has_output, HANDLE *hChildOutput, + HANDLE *hChildError) +{ + HANDLE hInputWriteTmp = NULL; + HANDLE hOutputReadTmp = NULL; + HANDLE hOutputWrite = NULL; + HANDLE hErrorWrite = NULL; + HANDLE hInputRead = NULL; + m_hOutputRead = NULL; + m_hInputWrite = NULL; + memset(&m_oOutputRead, 0, sizeof(m_oOutputRead)); + memset(&m_oInputWrite, 0, sizeof(m_oInputWrite)); + + // manual reset event + m_oOutputRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (m_oOutputRead.hEvent == INVALID_HANDLE_VALUE) { + goto errout; + } + + // manual reset event + m_oInputWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (m_oInputWrite.hEvent == INVALID_HANDLE_VALUE) { + goto errout; + } + + if (has_output) { + // src for this code: https://www.daniweb.com/software-development/cpp/threads/295780/using-named-pipes-with-asynchronous-io-redirection-to-winapi + // ONLY IMPORTANT CHANGE + // set inheritance flag to TRUE in CreateProcess + // you need this for the client to inherit the handles + + SECURITY_ATTRIBUTES sa; + // Set up the security attributes struct. + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; // this is the critical bit + sa.lpSecurityDescriptor = NULL; + + // Create the child output named pipe. + // This creates a inheritable, one-way handle for the server to read + hOutputReadTmp = CreateNamedPipe( + TEXT("\\\\.\\pipe\\outstreamPipe"), + PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + 1, 4096, 4096, 0, &sa); + if (hOutputReadTmp == INVALID_HANDLE_VALUE) { + goto errout; + } + + + // However, the client can not use an inbound server handle. + // Client needs a write-only, outgoing handle. + // So, you create another handle to the same named pipe, only + // write-only. Again, must be created with the inheritable + // attribute, and the options are important. + // use CreateFile to open a new handle to the existing pipe... + hOutputWrite = CreateFile( + TEXT("\\\\.\\pipe\\outstreamPipe"), + FILE_WRITE_DATA | SYNCHRONIZE, + 0, &sa, OPEN_EXISTING, // very important flag! + FILE_ATTRIBUTE_NORMAL, 0 // no template file for OPEN_EXISTING + ); + + + // All is well? not quite. Our main server-side handle was + // created shareable. + // That means the client will receive it, and we have a + // problem because pipe termination conditions rely on knowing + // when the last handle closes. + // So, the only answer is to create another one, just for the server... + // Create new output read handle and the input write + // handles. Set the Inheritance property to FALSE. Otherwise, + // the child inherits the properties and, as a result, + // non-closeable handles to the pipes are created. + if (!DuplicateHandle(GetCurrentProcess(), hOutputReadTmp, + GetCurrentProcess(), &m_hOutputRead, + 0, FALSE, // Make it uninheritable. + DUPLICATE_SAME_ACCESS)) { + goto errout; + } + // now we kill the original, inheritable server-side handle. That + // will keep the pipe open, but keep the client program from + // getting it. Child-proofing. Close inheritable copies of the + // handles you do not want to be inherited. + if (!CloseHandle(hOutputReadTmp)) { + goto errout; + } + hOutputReadTmp = NULL; + } + + +#if 0 // Nope, can't do this, we certainly don't want to mix stdout and stderr + // Have to take the give stderr output file instead + if (!m->m_stderrFile.empty()) { + // Open the file and make the handle inheritable + } else { + // Duplicate our own stderr. Does passing NULL work for this ? + } + + // we're going to give the same pipe for stderr, so duplicate for that: + // Create a duplicate of the output write handle for the std error + // write handle. This is necessary in case the child application + // closes one of its std output handles. + if (!DuplicateHandle(GetCurrentProcess(), hOutputWrite, + GetCurrentProcess(), &hErrorWrite, + 0, TRUE, DUPLICATE_SAME_ACCESS)) { + goto errout; + } +#endif + + if (has_input) { + // now same procedure for input pipe + + HANDLE hInputWriteTmp = CreateNamedPipe( + TEXT("\\\\.\\pipe\\instreamPipe"), + PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + 1, 4096, 4096, 0, &sa); + if (hInputWriteTmp == INVALID_HANDLE_VALUE) { + goto errout; + } + + hInputRead = CreateFile( + TEXT("\\\\.\\pipe\\instreamPipe"), + FILE_READ_DATA | SYNCHRONIZE, + 0, &sa, OPEN_EXISTING, // very important flag! + FILE_ATTRIBUTE_NORMAL, 0 // no template file for OPEN_EXISTING + ); + + if (!DuplicateHandle(GetCurrentProcess(), hInputWriteTmp, + GetCurrentProcess(), &m_hInputWrite, + 0, FALSE, // Make it uninheritable. + DUPLICATE_SAME_ACCESS)) { + goto errout; + } + if (!CloseHandle(hInputWriteTmp)) { + goto errout; + } + hInputWriteTmp = NULL; + } + + *hChildInput = hInputRead; + *hChildOutput = hOutputWrite; + *hChildError = hErrorWrite; + return true; + +errout: + if (hInputWriteTmp) + CloseHandle(hInputWriteTmp); + if (hOutputReadTmp) + CloseHandle(hOutputReadTmp); + if (m_hOutputRead) + CloseHandle(m_hOutputRead); + if (m_hInputWrite) + CloseHandle(m_hInputWrite); + if (m_oOutputRead.hEvent) + CloseHandle(m_oOutputRead.hEvent); + if (m_oInputWrite.hEvent) + CloseHandle(m_oInputWrite.hEvent); + if (hOutputWrite) + CloseHandle(hOutputWrite); + if (hInputRead) + CloseHandle(hInputRead); + if (hErrorWrite) + CloseHandle(hErrorWrite); +} + +/** + This routine appends the given argument to a command line such + that CommandLineToArgvW will return the argument string unchanged. + Arguments in a command line should be separated by spaces; this + function does not add these spaces. The caller must append spaces + between calls. + + @param arg Supplies the argument to encode. + + @param cmdLine Supplies the command line to which we append + the encoded argument string. + + @force Supplies an indication of whether we should quote + the argument even if it does not contain any characters that + would ordinarily require quoting. +*/ +static void argQuote(const string& arg, + string& cmdLine, + bool force = false) +{ + // Don't quote unless we actually need to do so + if (!force && !arg.empty() && + arg.find_first_of(" \t\n\v\"") == arg.npos) { + cmdLine.append(arg); + } else { + cmdLine.push_back ('"'); + + for (auto It = arg.begin () ; ; ++It) { + unsigned NumberBackslashes = 0; + + while (It != arg.end () && *It == '\\') { + ++It; + ++NumberBackslashes; + } + + if (It == arg.end()) { + // Escape all backslashes, but let the terminating + // double quotation mark we add below be interpreted + // as a metacharacter. + cmdLine.append (NumberBackslashes * 2, '\\'); + break; + } else if (*It == L'"') { + // Escape all backslashes and the following + // double quotation mark. + cmdLine.append (NumberBackslashes * 2 + 1, '\\'); + cmdLine.push_back (*It); + } else { + // Backslashes aren't special here. + cmdLine.append (NumberBackslashes, '\\'); + cmdLine.push_back (*It); + } + } + cmdLine.push_back ('"'); + } +} + +static string argvToCmdLine(const string& cmd, const vector& args) +{ + string cmdline; + argQuote(cmd, cmdline); + for (auto it = args.begin(); it != args.end(); it++) { + cmdline.append(" "); + argQuote(cmd, cmdline); + } + return cmdline; +} + + +// Create a child process +int ExecCmd::startExec(const string &cmd, const vector& args, + bool has_input, bool has_output) +{ + { // Debug and logging + string command = cmd + " "; + for (vector::const_iterator it = args.begin(); + it != args.end(); it++) { + command += "{" + *it + "} "; + } + LOGDEB(("ExecCmd::startExec: (%d|%d) %s\n", + has_input, has_output, command.c_str())); + } + + string cmdline = argvToCmdLine(cmd, args); + + // The resource manager ensures resources are freed if we return early + //ExecCmdRsrc e(this->m); + + HANDLE hInputRead; + HANDLE hOutputWrite; + HANDLE hErrorWrite; + if (!m->PreparePipes(has_input, &hInputRead, has_output, + &hOutputWrite, &hErrorWrite)) { + return false; + } + + STARTUPINFO siStartInfo; + BOOL bSuccess = FALSE; + + // Set up members of the PROCESS_INFORMATION structure. + + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); + + // Set up members of the STARTUPINFO structure. + // This structure specifies the STDIN and STDOUT handles for redirection. + + ZeroMemory(&siStartInfo, sizeof(STARTUPINFO)); + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + siStartInfo.hStdOutput = hOutputWrite; + siStartInfo.hStdInput = hInputRead; + siStartInfo.hStdError = hErrorWrite; + + // Create the child process. + bSuccess = CreateProcess(NULL, + cmdline.c_str(), // command line + NULL, // process security attributes + NULL, // primary thread security attrs + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &piProcInfo); // receives PROCESS_INFORMATION + if (!bSuccess) { + return false; + } + m->pid = piProcInfo.dwProcessId; + return true; +} + + + +// Read from a file and write its contents to the pipe for the child's +// STDIN. Stop when there is no more data. +int ExecCmd::send(const string& data) +{ + DWORD dwRead, dwWritten; + BOOL bSuccess = false; + + bSuccess = WriteFile(m->m_hInputWrite, data.c_str(), data.size(), + NULL, &m->m_oInputWrite); + DWORD err = GetLastError(); + + // TODO: some more decision, either the operation completes immediately + // and we get success, or it is started (which is indicated by no success) + // and ERROR_IO_PENDING + // in the first case bytes read/written parameter can be used directly + if (!bSuccess && err != ERROR_IO_PENDING) { + return; + } + + WaitResult waitRes = Wait(oInputWrite.hEvent, 5000); + if (waitRes == Ok) { + if (!GetOverlappedResult(m->m_hInputWrite, + &m->m_oInputWrite, &dwWritten, TRUE)) { + ErrorExit("GetOverlappedResult"); + } + } else if (waitRes == Quit) { + if (!CancelIo(hInputWrite)) + ErrorExit("CancelIo"); + return; + } else if (waitRes == Timeout) { + if (!CancelIo(hInputWrite)) + ErrorExit("CancelIo"); + return; + } +} + +// Read output from the child process's pipe for STDOUT +// and write to cout in this programme +// Stop when there is no more data. +// @arg cnt count to read, -1 means read to end of data. +int ExecCmd::receive(const string& data, int cnt) +{ + DWORD dwRead; + const int BUFSIZE = 8192; + CHAR chBuf[BUFSIZE]; + BOOL bSuccess = FALSE; + int totread = 0; + + while (true) { + int toread = cnt > 0 ? MIN(cnt - totread, BUFSIZE) : BUFSIZE; + bSuccess = ReadFile(m->m_hOutputRead, chBuf, toread, + NULL, &m->m_oOutputRead); + DWORD err = GetLastError(); + if (!bSuccess && err != ERROR_IO_PENDING) + break; + + WaitResult waitRes = Wait(oOutputRead.hEvent, 5000); + if (waitRes == Ok) { + if (!GetOverlappedResult(hOutputRead, &oOutputRead, &dwRead, TRUE)) { + ErrorExit("GetOverlappedResult"); + } + totread += dwRead; + data.append(buf, dwRead); + } else if (waitRes == Quit) { + if (!CancelIo(hOutputRead)) + ErrorExit("CancelIo"); + break; + } else if (waitRes == Timeout) { + if (!CancelIo(hOutputRead)) + ErrorExit("CancelIo"); + break; + } + } + return totread; +} + +int ExecCmd::wait() +{ + // Wait until child process exits. + WaitForSingleObject(m->m_piProcInfo.hProcess, INFINITE); + + // Get exit code + DWORD exit_code = 0; + GetExitCodeProcess(m->m_piProcInfo.hProcess, &exit_code); + + // Close handles to the child process and its primary thread. + CloseHandle(m->m_piProcInfo.hProcess); + CloseHandle(m->m_piProcInfo.hThread); + return (int)exit_code; +} + + +int ExecCmd::doexec(const string &cmd, const vector& args, + const string *input, string *output) +{ + if (input && output) { + LOGERR(("ExecCmd::doexec: can't do both input and output\n")); + return -1; + } + if (startExec(cmd, args, input != 0, output != 0) < 0) { + return -1; + } + if (input) { + receive(*input); + } else if (output) { + send(*output); + } + return wait(); +}