mijin2/source/mijin/io/process.cpp
Patrick Wuttke a1f1717e22 Added ProcessStream to for running processes (on Linux) and streaming their output.
Added map() function for creating a mapping iterator.
Added mijin::pipe types for map() and join().
2023-08-06 13:52:51 +02:00

176 lines
3.9 KiB
C++

#include "./process.hpp"
#include <algorithm>
#include <sstream>
#include "../util/iterators.hpp"
#include "../util/string.hpp"
namespace mijin
{
ProcessStream::~ProcessStream()
{
if (handle) {
close();
}
}
StreamError ProcessStream::open(const char* command, FileOpenMode mode_)
{
mode = mode_;
const char* modeStr; // NOLINT(cppcoreguidelines-init-variables)
switch (mode_)
{
case FileOpenMode::READ:
modeStr = "r";
break;
case FileOpenMode::WRITE:
modeStr = "w";
break;
case FileOpenMode::READ_WRITE:
modeStr = "rw";
break;
default:
assert(!"Unsupported mode for ProcessStream::open()");
return StreamError::NOT_SUPPORTED;
}
handle = popen(command, modeStr); // NOLINT(cppcoreguidelines-owning-memory)
if (!handle) {
return StreamError::IO_ERROR;
}
return StreamError::SUCCESS;
}
int ProcessStream::close()
{
assert(handle);
const int result = pclose(handle);
handle = nullptr;
return result;
}
StreamError ProcessStream::readRaw(std::span<std::uint8_t> buffer, bool partial, std::size_t* outBytesRead)
{
assert(handle);
assert(mode == FileOpenMode::READ || mode == FileOpenMode::READ_WRITE);
if (bufferedChar >= 0 && buffer.size() > 0)
{
buffer[0] = static_cast<std::uint8_t>(bufferedChar);
}
const std::size_t readBytes = std::fread(buffer.data(), 1, buffer.size(), handle);
if (std::ferror(handle)) {
return StreamError::IO_ERROR;
}
if (!partial && readBytes < buffer.size()) {
return StreamError::IO_ERROR;
}
if (outBytesRead != nullptr)
{
*outBytesRead = readBytes + (bufferedChar >= 0 ? 1 : 0);
}
bufferedChar = -1;
return StreamError::SUCCESS;
}
StreamError ProcessStream::writeRaw(std::span<const std::uint8_t> buffer)
{
assert(handle);
assert(mode == FileOpenMode::WRITE || mode == FileOpenMode::READ_WRITE);
const std::size_t written = std::fwrite(buffer.data(), 1, buffer.size(), handle);
if (written != buffer.size() || std::ferror(handle)) {
return StreamError::IO_ERROR;
}
return StreamError::SUCCESS;
}
std::size_t ProcessStream::tell()
{
assert(handle);
return std::ftell(handle); // TODO: does this work?
}
StreamError ProcessStream::seek(std::intptr_t pos, SeekMode seekMode)
{
(void) pos;
(void) seekMode;
return StreamError::NOT_SUPPORTED;
}
void ProcessStream::flush()
{
const int result = std::fflush(handle);
assert(result == 0);
}
bool ProcessStream::isAtEnd()
{
assert(handle);
if (bufferedChar >= 0) {
return false;
}
bufferedChar = std::fgetc(handle);
if (std::feof(handle)) {
return true;
}
return false;
}
StreamFeatures ProcessStream::getFeatures()
{
if (handle)
{
return {
.read = (mode == FileOpenMode::READ),
.write = (mode == FileOpenMode::WRITE || mode == FileOpenMode::READ_WRITE),
.tell = true,
.seek = false
};
}
return {};
}
std::string shellEscape(const std::string& arg) noexcept
{
std::ostringstream oss;
const bool requiresQuotes = std::any_of(arg.begin(), arg.end(), [&](const char chr) { return std::isspace(chr); });
if (requiresQuotes) {
oss << '"';
}
for (const char chr : arg)
{
// TODO: oh Windows this will probably be different
switch (chr)
{
case '"':
case '\'':
case '\\':
case '$':
oss << '\\';
break;
}
oss << chr;
}
if (requiresQuotes) {
oss << '"';
}
return oss.str();
}
std::string makeShellCommand(const std::vector<std::string>& args) noexcept
{
using namespace mijin::pipe;
return args
| Map(&shellEscape)
| Join(" ");
}
} // namespace mijin