A performance-oriented Lisp-like language where I can have my cake, and eat it (too)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

593 lines
16 KiB

#include "RunProcess.hpp"
#include <stdio.h>
#include <vector>
#ifdef UNIX
#include <string.h>
#include <sys/types.h> // pid
#include <sys/wait.h> // waitpid
#include <unistd.h> // exec, fork
#elif WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// _tprintf() Remove me!
#include <tchar.h>
#else
#error Platform support is needed for running subprocesses
#endif
#include "Logging.hpp"
#include "Utilities.hpp"
#ifdef UNIX
typedef pid_t ProcessId;
#else
typedef int ProcessId;
#endif
struct Subprocess
{
int* statusOut;
#ifdef UNIX
ProcessId processId;
int pipeReadFileDescriptor;
#elif WINDOWS
PROCESS_INFORMATION* processInfo;
// HANDLE hChildStd_IN_Wr; // Not used
HANDLE hChildStd_OUT_Rd;
#endif
std::string command;
};
static std::vector<Subprocess> s_subprocesses;
// Never returns, if success
void systemExecute(const char* fileToExecute, char** arguments)
{
#ifdef UNIX
// pid_t pid;
execvp(fileToExecute, arguments);
perror("RunProcess execvp() error: ");
Logf("Failed to execute %s\n", fileToExecute);
#endif
}
void subprocessReceiveStdOut(const char* processOutputBuffer)
{
Logf("%s", processOutputBuffer);
}
int runProcess(const RunProcessArguments& arguments, int* statusOut)
{
if (!arguments.arguments)
{
Log("error: runProcess() called with empty arguments. At a minimum, first argument must be "
"executable name\n");
return 1;
}
if (logging.processes)
{
Log("RunProcess command: ");
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
{
Logf("%s ", *arg);
}
Log("\n");
}
#ifdef UNIX
int pipeFileDescriptors[2] = {0};
const int PipeRead = 0;
const int PipeWrite = 1;
if (pipe(pipeFileDescriptors) == -1)
{
perror("RunProcess: ");
return 1;
}
pid_t pid = fork();
if (pid == -1)
{
perror("RunProcess fork() error: cannot create child: ");
return 1;
}
// Child
else if (pid == 0)
{
// Redirect std out and err to the pipes instead
if (dup2(pipeFileDescriptors[PipeWrite], STDOUT_FILENO) == -1 ||
dup2(pipeFileDescriptors[PipeWrite], STDERR_FILENO) == -1)
{
perror("RunProcess: ");
return 1;
}
// Only write
close(pipeFileDescriptors[PipeRead]);
char** nonConstArguments = nullptr;
{
int numArgs = 0;
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
++numArgs;
// Add one for null sentinel
nonConstArguments = new char*[numArgs + 1];
int i = 0;
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
{
nonConstArguments[i] = StrDuplicate(*arg);
++i;
}
// Null sentinel
nonConstArguments[numArgs] = nullptr;
}
if (arguments.workingDirectory)
{
if (chdir(arguments.workingDirectory) != 0)
{
Logf("error: RunProcess failed to change directory to '%s'\n",
arguments.workingDirectory);
perror("RunProcess chdir");
goto childProcessFailed;
}
if (logging.processes)
Logf("Set working directory to %s\n", arguments.workingDirectory);
}
systemExecute(arguments.fileToExecute, nonConstArguments);
childProcessFailed:
// This shouldn't happen unless the execution failed or soemthing
for (char** arg = nonConstArguments; *arg != nullptr; ++arg)
delete *arg;
delete[] nonConstArguments;
// A failed child should not flush parent files
_exit(EXIT_FAILURE); /* */
}
// Parent
else
{
// Only read
close(pipeFileDescriptors[PipeWrite]);
if (logging.processes)
Logf("Created child process %d\n", pid);
std::string command = "";
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
{
command.append(*arg);
command.append(" ");
}
s_subprocesses.push_back({statusOut, pid, pipeFileDescriptors[PipeRead], command});
}
return 0;
#elif WINDOWS
const char* fileToExecute = arguments.fileToExecute;
// Build a single string with all arguments
char* commandLineString = nullptr;
{
size_t commandLineLength = 0;
bool isFirstArg = true;
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
{
if (isFirstArg)
{
commandLineLength += strlen(fileToExecute);
// Room for quotes
commandLineLength += 2;
isFirstArg = false;
}
else
commandLineLength += strlen(*arg);
// Room for space
commandLineLength += 1;
}
commandLineString = (char*)calloc(commandLineLength, sizeof(char));
commandLineString[commandLineLength - 1] = '\0';
char* writeHead = commandLineString;
isFirstArg = true;
for (const char** arg = arguments.arguments; *arg != nullptr; ++arg)
{
// Support executable with spaces in path
if (isFirstArg)
{
isFirstArg = false;
if (!writeCharToBuffer('"', &writeHead, commandLineString, commandLineLength))
{
Log("error: ran out of space to write command\n");
free(commandLineString);
return 1;
}
if (!writeStringToBuffer(fileToExecute, &writeHead, commandLineString,
commandLineLength))
{
Log("error: ran out of space to write command\n");
free(commandLineString);
return 1;
}
if (!writeCharToBuffer('"', &writeHead, commandLineString, commandLineLength))
{
Log("error: ran out of space to write command\n");
free(commandLineString);
return 1;
}
}
else
{
if (!writeStringToBuffer(*arg, &writeHead, commandLineString, commandLineLength))
{
Log("error: ran out of space to write command\n");
free(commandLineString);
return 1;
}
}
if (*(arg + 1) != nullptr)
{
if (!writeCharToBuffer(' ', &writeHead, commandLineString, commandLineLength))
{
Log("error: ran out of space to write command\n");
free(commandLineString);
return 1;
}
}
}
}
if (logging.processes)
Logf("Final command string: %s\n", commandLineString);
// Redirect child process std in/out
HANDLE hChildStd_IN_Rd = nullptr;
HANDLE hChildStd_IN_Wr = nullptr;
HANDLE hChildStd_OUT_Rd = nullptr;
HANDLE hChildStd_OUT_Wr = nullptr;
{
SECURITY_ATTRIBUTES securityAttributes;
// Set the bInheritHandle flag so pipe handles are inherited.
securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
securityAttributes.bInheritHandle = TRUE;
securityAttributes.lpSecurityDescriptor = NULL;
// Create a pipe for the child process's STDOUT.
if (!CreatePipe(&hChildStd_OUT_Rd, &hChildStd_OUT_Wr, &securityAttributes, 0))
{
Logf("StdoutRd CreatePipe error %d\n", GetLastError());
free(commandLineString);
return 1;
}
// Ensure the read handle to the pipe for STDOUT is not inherited.
if (!SetHandleInformation(hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
{
Logf("Stdout SetHandleInformation error %d\n", GetLastError());
free(commandLineString);
CloseHandle(hChildStd_OUT_Rd);
CloseHandle(hChildStd_OUT_Wr);
return 1;
}
// Create a pipe for the child process's STDIN.
if (!CreatePipe(&hChildStd_IN_Rd, &hChildStd_IN_Wr, &securityAttributes, 0))
{
Logf("Stdin CreatePipe error %d\n", GetLastError());
free(commandLineString);
CloseHandle(hChildStd_OUT_Rd);
CloseHandle(hChildStd_OUT_Wr);
return 1;
}
// Ensure the write handle to the pipe for STDIN is not inherited.
if (!SetHandleInformation(hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0))
{
Logf("Stdin SetHandleInformation error %d\n", GetLastError());
free(commandLineString);
CloseHandle(hChildStd_OUT_Rd);
CloseHandle(hChildStd_OUT_Wr);
CloseHandle(hChildStd_IN_Rd);
CloseHandle(hChildStd_IN_Wr);
return 1;
}
}
STARTUPINFO startupInfo;
ZeroMemory(&startupInfo, sizeof(startupInfo));
startupInfo.cb = sizeof(startupInfo);
// This structure specifies the STDIN and STDOUT handles for redirection.
startupInfo.hStdError = hChildStd_OUT_Wr;
startupInfo.hStdOutput = hChildStd_OUT_Wr;
startupInfo.hStdInput = hChildStd_IN_Rd;
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
PROCESS_INFORMATION* processInfo = new PROCESS_INFORMATION;
ZeroMemory(processInfo, sizeof(PROCESS_INFORMATION));
// Start the child process.
if (!CreateProcess(fileToExecute,
commandLineString, // Command line
nullptr, // No security attributes
nullptr, // Thread handle not inheritable
true, // Set handle inheritance to true
0, // No creation flags
nullptr, // Use parent's environment block
arguments.workingDirectory, // If nullptr, use parent's starting directory
&startupInfo, // Pointer to STARTUPINFO structure
processInfo)) // Pointer to PROCESS_INFORMATION structure
{
CloseHandle(hChildStd_OUT_Rd);
CloseHandle(hChildStd_OUT_Wr);
CloseHandle(hChildStd_IN_Rd);
CloseHandle(hChildStd_IN_Wr);
free(commandLineString);
int errorCode = GetLastError();
if (errorCode == ERROR_FILE_NOT_FOUND)
{
Logf("CreateProcess failed to find file: %s\n", fileToExecute);
}
else if (errorCode == ERROR_PATH_NOT_FOUND)
{
Logf("CreateProcess failed to find path: %s\n", fileToExecute);
}
else
{
Logf("CreateProcess failed: %d\n", errorCode);
}
// LogLastError();
return 1;
}
// Close handles to the stdin and stdout pipes no longer needed by the child process.
// If they are not explicitly closed, there is no way to recognize that the child process ended
CloseHandle(hChildStd_OUT_Wr);
CloseHandle(hChildStd_IN_Rd);
CloseHandle(hChildStd_IN_Wr);
Subprocess newProcess = {0};
newProcess.statusOut = statusOut;
newProcess.processInfo = processInfo;
newProcess.hChildStd_OUT_Rd = hChildStd_OUT_Rd;
newProcess.command = commandLineString;
s_subprocesses.push_back(std::move(newProcess));
free(commandLineString);
return 0;
#endif
return 1;
}
#ifdef WINDOWS
void readProcessPipe(Subprocess& process, SubprocessOnOutputFunc onOutput)
{
HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
char buffer[4096] = {0};
bool encounteredError = false;
while (true)
{
DWORD bytesRead = 0;
DWORD bytesWritten = 0;
bool success = ReadFile(process.hChildStd_OUT_Rd, buffer, sizeof(buffer), &bytesRead, NULL);
if (!success || bytesRead == 0)
{
encounteredError = !success;
break;
}
success = WriteFile(hParentStdOut, buffer, bytesRead, &bytesWritten, NULL);
if (!success)
{
encounteredError = true;
break;
}
if (onOutput && bytesRead <= sizeof(buffer))
onOutput(buffer);
}
// This seems to give a lot of false-positives
// if (encounteredError)
// Log("warning: encountered read or write error while receiving sub-process "
// "output\n");
}
#endif
// This function prints all the output for each process in one contiguous block, so that outputs
// between two processes aren't mangled together terribly
void waitForAllProcessesClosed(SubprocessOnOutputFunc onOutput)
{
if (s_subprocesses.empty())
return;
for (Subprocess& process : s_subprocesses)
{
#ifdef UNIX
char processOutputBuffer[1024] = {0};
int numBytesRead =
read(process.pipeReadFileDescriptor, processOutputBuffer, sizeof(processOutputBuffer));
while (numBytesRead > 0)
{
processOutputBuffer[numBytesRead] = '\0';
subprocessReceiveStdOut(processOutputBuffer);
if (onOutput)
onOutput(processOutputBuffer);
numBytesRead = read(process.pipeReadFileDescriptor, processOutputBuffer,
sizeof(processOutputBuffer));
}
close(process.pipeReadFileDescriptor);
waitpid(process.processId, process.statusOut, 0);
// It's pretty useful to see the command which resulted in failure
if (*process.statusOut != 0)
Logf("%s\n", process.command.c_str());
#elif WINDOWS
// We cannot wait indefinitely because the process eventually waits for us to read from the
// output pipe (e.g. its buffer gets full). pollProcessTimeMilliseconds may need to be
// tweaked for better performance; if the buffer is full, the subprocess will wait for as
// long as pollProcessTimeMilliseconds - time taken to fill buffer. Very low wait times will
// mean Cakelisp unnecessarily taking up cycles, so it's a tradeoff.
const int pollProcessTimeMilliseconds = 50;
while (WAIT_TIMEOUT ==
WaitForSingleObject(process.processInfo->hProcess, pollProcessTimeMilliseconds))
readProcessPipe(process, onOutput);
// If the wait was ended but wasn't a timeout, we still need to read out
readProcessPipe(process, onOutput);
DWORD exitCode = 0;
if (!GetExitCodeProcess(process.processInfo->hProcess, &exitCode))
{
Log("error: failed to get exit code for process\n");
exitCode = 1;
}
else if (exitCode != 0)
{
Logf("%s\n", process.command.c_str());
}
*process.statusOut = exitCode;
// Close process, thread, and stdout handles.
CloseHandle(process.processInfo->hProcess);
CloseHandle(process.processInfo->hThread);
CloseHandle(process.hChildStd_OUT_Rd);
#endif
}
s_subprocesses.clear();
}
void PrintProcessArguments(const char** processArguments)
{
for (const char** argument = processArguments; *argument; ++argument)
Logf("%s ", *argument);
Log("\n");
}
static const char* ProcessCommandArgumentTypeToString(ProcessCommandArgumentType type)
{
switch (type)
{
case ProcessCommandArgumentType_None:
return "None";
case ProcessCommandArgumentType_String:
return "String";
case ProcessCommandArgumentType_SourceInput:
return "SourceInput";
case ProcessCommandArgumentType_ObjectOutput:
return "ObjectOutput";
case ProcessCommandArgumentType_DebugSymbolsOutput:
return "DebugSymbolsOutput";
case ProcessCommandArgumentType_ImportLibraryPaths:
return "ImportLibraryPaths";
case ProcessCommandArgumentType_ImportLibraries:
return "ImportLibraries";
case ProcessCommandArgumentType_CakelispHeadersInclude:
return "CakelispHeadersInclude";
case ProcessCommandArgumentType_IncludeSearchDirs:
return "IncludeSearchDirs";
case ProcessCommandArgumentType_AdditionalOptions:
return "AdditionalOptions";
case ProcessCommandArgumentType_PrecompiledHeaderOutput:
return "PrecompiledHeaderOutput";
case ProcessCommandArgumentType_PrecompiledHeaderInclude:
return "PrecompiledHeaderInclude";
case ProcessCommandArgumentType_ObjectInput:
return "ObjectInput";
case ProcessCommandArgumentType_DynamicLibraryOutput:
return "DynamicLibraryOutput";
case ProcessCommandArgumentType_LibrarySearchDirs:
return "LibrarySearchDirs";
case ProcessCommandArgumentType_Libraries:
return "Libraries";
case ProcessCommandArgumentType_LibraryRuntimeSearchDirs:
return "LibraryRuntimeSearchDirs";
case ProcessCommandArgumentType_LinkerArguments:
return "LinkerArguments";
case ProcessCommandArgumentType_ExecutableOutput:
return "ExecutableOutput";
default:
return "Unknown";
}
}
// The array will need to be deleted, but the array members will not
const char** MakeProcessArgumentsFromCommand(const char* fileToExecute,
std::vector<ProcessCommandArgument>& arguments,
const ProcessCommandInput* inputs, int numInputs)
{
std::vector<const char*> argumentsAccumulate;
for (unsigned int i = 0; i < arguments.size(); ++i)
{
ProcessCommandArgument& argument = arguments[i];
if (argument.type == ProcessCommandArgumentType_String)
argumentsAccumulate.push_back(argument.contents.c_str());
else
{
bool found = false;
for (int input = 0; input < numInputs; ++input)
{
if (inputs[input].type == argument.type)
{
for (const char* value : inputs[input].value)
{
if (!value || !value[0])
{
Logf(
"warning: attempted to pass null string to '%s' under argument "
"type %s. It will be ignored\n",
fileToExecute, ProcessCommandArgumentTypeToString(argument.type));
continue;
}
argumentsAccumulate.push_back(value);
}
found = true;
break;
}
}
if (!found)
{
Logf("error: command to %s missing ProcessCommandInput of type %s\n", fileToExecute,
ProcessCommandArgumentTypeToString(argument.type));
return nullptr;
}
}
}
int numUserArguments = argumentsAccumulate.size();
// +1 for file to execute
int numFinalArguments = numUserArguments + 1;
// +1 again for the null terminator
const char** newArguments = (const char**)calloc(sizeof(const char*), numFinalArguments + 1);
newArguments[0] = fileToExecute;
for (int i = 1; i < numFinalArguments; ++i)
newArguments[i] = argumentsAccumulate[i - 1];
newArguments[numFinalArguments] = nullptr;
return newArguments;
}
#include <thread>
const int maxProcessesRecommendedSpawned = std::thread::hardware_concurrency();