/*
* Win32ChildProcess.cpp
*
* Copyright (C) 2022 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
#include "ChildProcessSubprocPoll.hpp"
#include "Win32Pty.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "CriticalSection.hpp"
namespace rstudio {
namespace core {
namespace system {
namespace {
// how long we keep "saw activity" state at true even if we haven't seen
// new activity
const boost::posix_time::milliseconds kResetRecentDelay =
boost::posix_time::milliseconds(1000);
// how often we update "has subprocesses" flag
const boost::posix_time::milliseconds kCheckSubprocDelay =
boost::posix_time::milliseconds(200);
// how often we query and store current working directory of subprocess
const boost::posix_time::milliseconds kCheckCwdDelay =
boost::posix_time::milliseconds(2000);
std::string findOnPath(const std::string& exe,
const std::string& appendExt = "")
{
// make sure it has the specified extension
std::string resolvedExe = exe;
if (!appendExt.empty() &&
!boost::algorithm::ends_with(resolvedExe, appendExt))
{
resolvedExe += appendExt;
}
// do the search
std::vector exeBuffer(MAX_PATH*4);
exeBuffer.insert(exeBuffer.begin(), resolvedExe.begin(), resolvedExe.end());
exeBuffer.push_back('\0');
if (::PathFindOnPath(&(exeBuffer[0]), nullptr))
{
return std::string(&(exeBuffer[0]));
}
else
{
return std::string();
}
}
// resolve the passed command and arguments to the form required for a
// call to CreateProcess (do path lookup if necessary and invoke the
// command within a command processor if it is a batch file)
void resolveCommand(std::string* pExecutable, std::vector* pArgs)
{
// if this is a root path or it exists then leave it as is
if (!FilePath::isRootPath(*pExecutable) && !FilePath::exists(*pExecutable))
{
// try to find it on the path as a .exe
std::string exePath = findOnPath(*pExecutable, ".exe");
if (!exePath.empty())
{
*pExecutable = exePath;
}
else
{
// try to find it on the path as a cmd
std::string cmdPath = findOnPath(*pExecutable, ".cmd");
if (!cmdPath.empty())
{
// set the pCmd to cmd.exe
std::string cmdExePath = findOnPath("cmd.exe");
if (!cmdExePath.empty())
{
// set to cmd.exe
*pExecutable = cmdExePath;
// manipulate args to have cmd.exe invoke the batch file
pArgs->insert(pArgs->begin(), cmdPath);
pArgs->insert(pArgs->begin(), "/C");
}
}
}
}
}
Error readPipeUntilDone(HANDLE hPipe, std::string* pOutput)
{
CHAR buff[1024];
DWORD nBytesRead;
while(TRUE)
{
// read from pipe
BOOL result = ::ReadFile(hPipe, buff, sizeof(buff), &nBytesRead, nullptr);
auto lastErr = ::GetLastError();
// end of file
if (nBytesRead == 0)
break;
// pipe broken
else if (!result && (lastErr == ERROR_BROKEN_PIPE))
break;
// unexpected error
else if (!result)
return systemError(lastErr, ERROR_LOCATION);
// got input, append it
else
pOutput->append(buff, nBytesRead);
}
return Success();
}
} // anonymous namespace
struct ChildProcess::Impl
{
Impl()
: hStdInWrite(nullptr),
hStdOutRead(nullptr),
hStdErrRead(nullptr),
hProcess(nullptr),
closeStdIn_(&hStdInWrite, ERROR_LOCATION),
closeStdOut_(&hStdOutRead, ERROR_LOCATION),
closeStdErr_(&hStdErrRead, ERROR_LOCATION),
closeProcess_(&hProcess, ERROR_LOCATION),
pid(static_cast(-1)),
ctrlC(0x03),
terminated(false)
{
}
public:
HANDLE hStdInWrite;
HANDLE hStdOutRead;
HANDLE hStdErrRead;
HANDLE hProcess;
private:
CloseHandleOnExitScope closeStdIn_;
CloseHandleOnExitScope closeStdOut_;
CloseHandleOnExitScope closeStdErr_;
CloseHandleOnExitScope closeProcess_;
public:
PidType pid;
char ctrlC;
bool terminated;
WinPty pty;
};
ChildProcess::ChildProcess()
: pImpl_(new Impl())
{
}
void ChildProcess::init(const std::string& exe,
const std::vector& args,
const ProcessOptions& options)
{
exe_ = exe;
args_ = args;
options_ = options;
resolveCommand(&exe_, &args_);
if (!options.stdOutFile.isEmpty() || !options.stdErrFile.isEmpty())
{
LOG_ERROR_MESSAGE(
"stdOutFile/stdErrFile options cannot be used with runProgram");
}
}
void ChildProcess::init(const std::string& command,
const ProcessOptions& options)
{
exe_ = findOnPath("cmd.exe");
args_.push_back("/S");
args_.push_back("/C");
args_.push_back("\"" + command + "\"");
options_ = options;
}
// initialize for an interactive terminal
void ChildProcess::init(const ProcessOptions& options)
{
options_ = options;
exe_ = options_.shellPath.getAbsolutePathNative();
args_ = options_.args;
}
ChildProcess::~ChildProcess()
{
}
Error ChildProcess::writeToStdin(const std::string& input, bool eof)
{
// write synchronously to the pipe
if (!input.empty())
{
if (options().pseudoterminal)
{
Error error = WinPty::writeToPty(pImpl_->hStdInWrite, input);
if (error)
return error;
}
else
{
DWORD dwWritten;
BOOL bSuccess = ::WriteFile(pImpl_->hStdInWrite,
input.data(),
static_cast(input.length()),
&dwWritten,
nullptr);
if (!bSuccess)
{
return LAST_SYSTEM_ERROR();
}
}
}
// close pipe if requested
if (eof)
return closeHandle(&pImpl_->hStdInWrite, ERROR_LOCATION);
else
return Success();
}
Error ChildProcess::ptySetSize(int cols, int rows)
{
// verify we are dealing with a pseudoterminal
if (!options().pseudoterminal)
return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
return pImpl_->pty.setSize(cols, rows);
}
Error ChildProcess::ptyInterrupt()
{
// verify we are dealing with a pseudoterminal
if (!options().pseudoterminal)
return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
return pImpl_->pty.interrupt();
}
PidType ChildProcess::getPid()
{
return pImpl_->pid;
}
Error ChildProcess::terminate()
{
// terminate with exit code 15 (15 is SIGTERM on posix)
if (!::TerminateProcess(pImpl_->hProcess, 15))
{
return LAST_SYSTEM_ERROR();
}
else
return Success();
}
bool ChildProcess::hasNonIgnoredSubprocess() const
{
// base class doesn't support subprocess-checking; override to implement
return true;
}
bool ChildProcess::hasIgnoredSubprocess() const
{
// base class doesn't support subprocess-checking; override to implement
return false;
}
core::FilePath ChildProcess::getCwd() const
{
// base class doesn't support cwd-tracking; override to implement
return FilePath();
}
bool ChildProcess::hasRecentOutput() const
{
// base class doesn't support output activity detection; override to implement
return true;
}
namespace {
Error openFile(const FilePath& file, bool inheritable, HANDLE* phFile)
{
HANDLE hFile = ::CreateFileW(file.getAbsolutePathW().c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return LAST_SYSTEM_ERROR();
}
if (inheritable)
{
if (!::SetHandleInformation(hFile,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT))
{
Error err = LAST_SYSTEM_ERROR();
::CloseHandle(hFile);
return err;
}
}
*phFile = hFile;
return Success();
}
} // namespace
Error ChildProcess::run()
{
Error error;
// NOTE: if the run method is called from multiple threads in single app
// concurrently then a race condition can cause handles to get incorrectly
// directed. the workaround suggested by microsoft is to wrap the process
// creation code in a critical section. see this article for details:
// http://support.microsoft.com/kb/315939
static CriticalSection s_runCriticalSection;
CriticalSection::Scope csScope(s_runCriticalSection);
// pseudoterminal mode: use winpty to emulate Posix pseudoterminal
if (options_.pseudoterminal)
{
error = pImpl_->pty.start(exe_, args_, options_,
&pImpl_->hStdInWrite,
&pImpl_->hStdOutRead,
&pImpl_->hStdErrRead,
&pImpl_->hProcess);
if (!error)
{
pImpl_->pid = ::GetProcessId(pImpl_->hProcess);
}
return error;
}
// Standard input pipe
HANDLE hStdInRead;
if (!::CreatePipe(&hStdInRead, &pImpl_->hStdInWrite, nullptr, 0))
{
return LAST_SYSTEM_ERROR();
}
CloseHandleOnExitScope closeStdIn(&hStdInRead, ERROR_LOCATION);
if (!::SetHandleInformation(hStdInRead,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT))
{
return LAST_SYSTEM_ERROR();
}
// Standard output pipe
HANDLE hStdOutWrite;
if (!::CreatePipe(&pImpl_->hStdOutRead, &hStdOutWrite, nullptr, 0))
{
return LAST_SYSTEM_ERROR();
}
CloseHandleOnExitScope closeStdOut(&hStdOutWrite, ERROR_LOCATION);
if (!::SetHandleInformation(hStdOutWrite,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT) )
{
return LAST_SYSTEM_ERROR();
}
// Standard error pipe
HANDLE hStdErrWrite;
if (!::CreatePipe(&pImpl_->hStdErrRead, &hStdErrWrite, nullptr, 0))
{
return LAST_SYSTEM_ERROR();
}
CloseHandleOnExitScope closeStdErr(&hStdErrWrite, ERROR_LOCATION);
if (!::SetHandleInformation(hStdErrWrite,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT) )
{
return LAST_SYSTEM_ERROR();
}
// populate startup info
STARTUPINFOW si = { sizeof(STARTUPINFOW) };
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdOutput = hStdOutWrite;
si.hStdError = options_.redirectStdErrToStdOut ? hStdOutWrite
: hStdErrWrite;
si.hStdInput = hStdInRead;
HANDLE hStdOutWriteFile = INVALID_HANDLE_VALUE;
if (!options_.stdOutFile.isEmpty())
{
error = openFile(options_.stdOutFile, true, &hStdOutWriteFile);
if (error)
return error;
si.hStdOutput = hStdOutWriteFile;
}
CloseHandleOnExitScope closeStdOutFile(&hStdOutWriteFile, ERROR_LOCATION);
HANDLE hStdErrWriteFile = INVALID_HANDLE_VALUE;
if (!options_.stdErrFile.isEmpty())
{
error = openFile(options_.stdErrFile, true, &hStdErrWriteFile);
if (error)
return error;
si.hStdOutput = hStdErrWriteFile;
}
CloseHandleOnExitScope closeStdErrFile(&hStdErrWriteFile, ERROR_LOCATION);
// build command line
std::vector cmdLine;
bool exeQuot =
std::string::npos != exe_.find(' ') &&
std::string::npos == exe_.find('"');
if (exeQuot)
cmdLine.push_back(L'"');
std::wstring exeWide = string_utils::utf8ToWide(exe_);
std::copy(exeWide.begin(), exeWide.end(), std::back_inserter(cmdLine));
if (exeQuot)
cmdLine.push_back(L'"');
for (std::string& arg : args_)
{
cmdLine.push_back(L' ');
// This is kind of gross. Ideally we would be more deterministic
// than this.
bool quot =
std::string::npos != arg.find(' ') &&
std::string::npos == arg.find('"');
if (quot)
cmdLine.push_back(L'"');
std::wstring argWide = string_utils::utf8ToWide(arg);
std::copy(argWide.begin(), argWide.end(), std::back_inserter(cmdLine));
if (quot)
cmdLine.push_back(L'"');
}
cmdLine.push_back(L'\0');
// specify custom environment if requested
DWORD dwFlags = 0;
LPVOID lpEnv = nullptr;
std::vector envBlock;
if (options_.environment)
{
const Options& env = options_.environment.get();
for (const Option& envVar : env)
{
std::wstring key = string_utils::utf8ToWide(envVar.first);
std::wstring value = string_utils::utf8ToWide(envVar.second);
std::copy(key.begin(), key.end(), std::back_inserter(envBlock));
envBlock.push_back(L'=');
std::copy(value.begin(), value.end(), std::back_inserter(envBlock));
envBlock.push_back(L'\0');
}
envBlock.push_back(L'\0');
dwFlags |= CREATE_UNICODE_ENVIRONMENT;
lpEnv = &envBlock[0];
}
if (options_.createNewConsole)
{
dwFlags |= CREATE_NEW_CONSOLE;
si.dwFlags |= STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
}
else if (options_.detachProcess || options_.terminateChildren)
{
dwFlags |= DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
si.dwFlags |= STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
}
if (options_.breakawayFromJob)
dwFlags |= CREATE_BREAKAWAY_FROM_JOB;
std::wstring workingDir;
if (!options_.workingDir.isEmpty())
{
workingDir = string_utils::utf8ToWide(
options_.workingDir.getAbsolutePathNative());
}
// Start the child process.
PROCESS_INFORMATION pi;
::ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
BOOL success = ::CreateProcessW(
exeWide.c_str(), // Process
&(cmdLine[0]), // Command line
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
TRUE, // Set handle inheritance to TRUE
dwFlags, // Creation flags
lpEnv, // Environment block
// Use parent's starting directory
workingDir.empty() ? nullptr : workingDir.c_str(),
&si, // Pointer to STARTUPINFO structure
&pi); // Pointer to PROCESS_INFORMATION structure
if (!success)
{
return LAST_SYSTEM_ERROR();
}
// close thread handle on exit
CloseHandleOnExitScope closeThread(&pi.hThread, ERROR_LOCATION);
// save handle to process
pImpl_->hProcess = pi.hProcess;
pImpl_->pid = ::GetProcessId(pImpl_->hProcess);
// success
return Success();
}
Error SyncChildProcess::readStdOut(std::string* pOutput)
{
return readPipeUntilDone(pImpl_->hStdOutRead, pOutput);
}
Error SyncChildProcess::readStdErr(std::string* pOutput)
{
return readPipeUntilDone(pImpl_->hStdErrRead, pOutput);
}
Error SyncChildProcess::waitForExit(int* pExitStatus)
{
// wait
DWORD result = ::WaitForSingleObject(pImpl_->hProcess, INFINITE);
// check for error
if (result != WAIT_OBJECT_0)
{
if (result == WAIT_FAILED)
{
return LAST_SYSTEM_ERROR();
}
else
{
Error error = systemError(boost::system::errc::result_out_of_range,
ERROR_LOCATION);
error.addProperty("result", result);
return error;
}
}
else
{
// get exit code
DWORD dwStatus;
if (!::GetExitCodeProcess(pImpl_->hProcess, &dwStatus))
{
return LAST_SYSTEM_ERROR();
}
*pExitStatus = dwStatus;
return Success();
}
}
struct AsyncChildProcess::AsyncImpl
{
AsyncImpl()
: calledOnStarted_(false),
exited_(false)
{
}
bool calledOnStarted_;
bool exited_;
boost::scoped_ptr pSubprocPoll_;
};
AsyncChildProcess::AsyncChildProcess(const std::string& exe,
const std::vector& args,
const ProcessOptions& options)
: ChildProcess(), pAsyncImpl_(new AsyncImpl())
{
init(exe, args, options);
}
AsyncChildProcess::AsyncChildProcess(const std::string& command,
const ProcessOptions& options)
: ChildProcess(), pAsyncImpl_(new AsyncImpl())
{
init(command, options);
}
AsyncChildProcess::AsyncChildProcess(const ProcessOptions& options)
: ChildProcess(), pAsyncImpl_(new AsyncImpl())
{
init(options);
}
AsyncChildProcess::~AsyncChildProcess()
{
}
Error AsyncChildProcess::terminate()
{
return ChildProcess::terminate();
}
bool AsyncChildProcess::hasNonIgnoredSubprocess() const
{
if (pAsyncImpl_->pSubprocPoll_)
return pAsyncImpl_->pSubprocPoll_->hasNonIgnoredSubprocess();
else
return true;
}
bool AsyncChildProcess::hasIgnoredSubprocess() const
{
if (pAsyncImpl_->pSubprocPoll_)
return pAsyncImpl_->pSubprocPoll_->hasIgnoredSubprocess();
else
return false;
}
core::FilePath AsyncChildProcess::getCwd() const
{
if (pAsyncImpl_->pSubprocPoll_)
return pAsyncImpl_->pSubprocPoll_->getCwd();
else
return FilePath();
}
bool AsyncChildProcess::hasRecentOutput() const
{
if (pAsyncImpl_->pSubprocPoll_)
return pAsyncImpl_->pSubprocPoll_->hasRecentOutput();
else
return true;
}
void AsyncChildProcess::poll()
{
// skip polling if we're not on the main thread,
// and the process options request we run on the main thread only
if (enableCallbacksRequireMainThread() && options().callbacksRequireMainThread && !core::thread::isMainThread())
return;
// call onStarted if we haven't yet
if (!(pAsyncImpl_->calledOnStarted_))
{
// setup for subprocess polling
pAsyncImpl_->pSubprocPoll_.reset(new ChildProcessSubprocPoll(
pImpl_->pid,
kResetRecentDelay, kCheckSubprocDelay, kCheckCwdDelay,
options().reportHasSubprocs ? core::system::getSubprocesses : nullptr,
options().ignoredSubprocs,
options().trackCwd ? core::system::currentWorkingDir : nullptr));
if (callbacks_.onStarted)
callbacks_.onStarted(*this);
pAsyncImpl_->calledOnStarted_ = true;
}
// call onContinue
if (callbacks_.onContinue)
{
if (!callbacks_.onContinue(*this) && !pImpl_->terminated)
{
// terminate the process
Error error = terminate();
if (error)
LOG_ERROR(error);
pImpl_->terminated = true;
}
}
bool hasRecentOutput = false;
// check stdout
if (pImpl_->hStdOutRead)
{
std::string stdOut;
Error error = WinPty::readFromPty(pImpl_->hStdOutRead, &stdOut);
if (error)
reportError(error);
if (!stdOut.empty() && callbacks_.onStdout)
callbacks_.onStdout(*this, stdOut);
}
// check stderr
if (pImpl_->hStdErrRead)
{
std::string stdErr;
Error error = WinPty::readFromPty(pImpl_->hStdErrRead, &stdErr);
if (error)
reportError(error);
if (!stdErr.empty() && callbacks_.onStderr)
{
hasRecentOutput = true;
callbacks_.onStderr(*this, stdErr);
}
}
// check for process exit
DWORD result = ::WaitForSingleObject(pImpl_->hProcess, 0);
// check for process exit (or error waiting)
if (result != WAIT_TIMEOUT)
{
// try to get exit status
int exitStatus = -1;
// normal wait for process state
if (result == WAIT_OBJECT_0)
{
// get the exit status
DWORD dwStatus;
if (!::GetExitCodeProcess(pImpl_->hProcess, &dwStatus))
{
LOG_ERROR(LAST_SYSTEM_ERROR());
}
exitStatus = dwStatus;
}
// error state, return -1 and try to log a meaningful error
else
{
Error error;
if (result == WAIT_FAILED)
{
error = LAST_SYSTEM_ERROR();
}
else
{
error = systemError(boost::system::errc::result_out_of_range,
ERROR_LOCATION);
error.addProperty("result", result);
}
LOG_ERROR(error);
}
// read all remaining stdout
if (pImpl_->hStdOutRead)
{
std::string stdOut;
Error error = readPipeUntilDone(pImpl_->hStdOutRead, &stdOut);
if (error)
reportError(error);
if (!stdOut.empty() && callbacks_.onStdout)
callbacks_.onStdout(*this, stdOut);
}
// read all remaining stderr
if (pImpl_->hStdErrRead)
{
std::string stdErr;
Error error = readPipeUntilDone(pImpl_->hStdErrRead, &stdErr);
if (error)
reportError(error);
if (!stdErr.empty() && callbacks_.onStderr)
{
hasRecentOutput = true;
callbacks_.onStderr(*this, stdErr);
}
}
// close the process handle
Error error = closeHandle(&pImpl_->hProcess, ERROR_LOCATION);
if (error)
LOG_ERROR(error);
// call onExit
if (callbacks_.onExit)
callbacks_.onExit(exitStatus);
// set exited_ flag so that our exited function always
// returns the right value
pAsyncImpl_->exited_ = true;
pAsyncImpl_->pSubprocPoll_->stop();
}
// Perform optional periodic operations
if (pAsyncImpl_->pSubprocPoll_->poll(hasRecentOutput))
{
if (callbacks_.onHasSubprocs)
{
callbacks_.onHasSubprocs(hasNonIgnoredSubprocess(),
hasIgnoredSubprocess());
}
if (callbacks_.reportCwd)
{
callbacks_.reportCwd(getCwd());
}
}
}
bool AsyncChildProcess::exited()
{
return pImpl_->hProcess == nullptr;
}
} // namespace system
} // namespace core
} // namespace rstudio