feat(process): add managed process control
This commit is contained in:
@@ -10,6 +10,7 @@ target_sources(izo
|
||||
src/command_line.cpp
|
||||
src/dialogs.cpp
|
||||
src/directory_watcher.cpp
|
||||
src/process.cpp
|
||||
src/time.cpp
|
||||
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:src/platform_windows.cpp>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace izo {
|
||||
@@ -12,6 +16,10 @@ struct ProcessOptions {
|
||||
std::vector<std::string> arguments;
|
||||
std::filesystem::path workingDirectory;
|
||||
bool detached = false;
|
||||
bool captureOutput = false;
|
||||
bool pipeStdin = false;
|
||||
// Used by RunProcess. A negative value waits indefinitely.
|
||||
std::int64_t timeoutMs = -1;
|
||||
};
|
||||
|
||||
struct ProcessResult {
|
||||
@@ -23,4 +31,55 @@ struct ProcessResult {
|
||||
|
||||
ProcessResult LaunchProcess(const ProcessOptions& options);
|
||||
|
||||
class Process {
|
||||
public:
|
||||
Process() = default;
|
||||
~Process();
|
||||
Process(Process&&) noexcept = default;
|
||||
Process& operator=(Process&& other) noexcept;
|
||||
Process(const Process&) = delete;
|
||||
Process& operator=(const Process&) = delete;
|
||||
|
||||
explicit operator bool() const noexcept { return state_ != nullptr; }
|
||||
std::uint32_t Id() const noexcept;
|
||||
bool IsRunning() const;
|
||||
// Returns false if the timeout expires or this Process is invalid.
|
||||
bool Wait(std::int64_t timeoutMs = -1, std::string* errorMessage = nullptr);
|
||||
// Forcefully terminates the child process.
|
||||
bool Terminate(std::string* errorMessage = nullptr);
|
||||
std::optional<int> ExitCode() const;
|
||||
|
||||
bool WriteStdin(std::string_view data, std::string* errorMessage = nullptr);
|
||||
bool CloseStdin(std::string* errorMessage = nullptr);
|
||||
// Captured output is complete after Wait succeeds.
|
||||
std::string Stdout() const;
|
||||
std::string Stderr() const;
|
||||
|
||||
private:
|
||||
struct State;
|
||||
explicit Process(std::shared_ptr<State> state) : state_(std::move(state)) {}
|
||||
std::shared_ptr<State> state_;
|
||||
|
||||
friend Process StartProcess(const ProcessOptions&, std::string*);
|
||||
};
|
||||
|
||||
Process StartProcess(const ProcessOptions& options, std::string* errorMessage = nullptr);
|
||||
|
||||
struct RunProcessResult {
|
||||
bool started = false;
|
||||
bool timedOut = false;
|
||||
int exitCode = -1;
|
||||
std::string stdoutText;
|
||||
std::string stderrText;
|
||||
std::string errorMessage;
|
||||
|
||||
explicit operator bool() const noexcept { return started && !timedOut && exitCode == 0; }
|
||||
};
|
||||
|
||||
RunProcessResult RunProcess(const ProcessOptions& options);
|
||||
|
||||
bool IsProcessRunning(std::uint32_t processId) noexcept;
|
||||
std::uint32_t GetCurrentProcessId() noexcept;
|
||||
std::uint32_t GetParentProcessId() noexcept;
|
||||
|
||||
} // namespace izo
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#ifdef __linux__
|
||||
@@ -17,6 +18,8 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -103,6 +106,21 @@ std::string read_first_line(const std::filesystem::path& path) {
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Process::State {
|
||||
~State() { if (stdinWrite >= 0) close(stdinWrite); }
|
||||
|
||||
pid_t processId = -1;
|
||||
int stdinWrite = -1;
|
||||
mutable std::mutex mutex;
|
||||
std::mutex stdinMutex;
|
||||
std::condition_variable changed;
|
||||
bool finished = false;
|
||||
int readers = 0;
|
||||
int exitCode = -1;
|
||||
std::string stdoutText;
|
||||
std::string stderrText;
|
||||
};
|
||||
|
||||
std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||
switch (path) {
|
||||
case KnownPath::AppData: return env_path("XDG_DATA_HOME", home() / ".local/share");
|
||||
@@ -133,24 +151,50 @@ std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
||||
if (options.executable.empty()) return {false, 0, "Executable path is required"};
|
||||
int error_pipe[2];
|
||||
if (pipe2(error_pipe, O_CLOEXEC) != 0) return {false, 0, std::strerror(errno)};
|
||||
Process StartProcess(const ProcessOptions& options, std::string* errorMessage) {
|
||||
if (options.executable.empty()) {
|
||||
if (errorMessage) *errorMessage = "Executable path is required";
|
||||
return {};
|
||||
}
|
||||
int error_pipe[2] = {-1, -1};
|
||||
int stdout_pipe[2] = {-1, -1};
|
||||
int stderr_pipe[2] = {-1, -1};
|
||||
int stdin_pipe[2] = {-1, -1};
|
||||
const auto close_pipe = [](int (&pipe_value)[2]) {
|
||||
if (pipe_value[0] >= 0) close(pipe_value[0]);
|
||||
if (pipe_value[1] >= 0) close(pipe_value[1]);
|
||||
pipe_value[0] = pipe_value[1] = -1;
|
||||
};
|
||||
if (pipe2(error_pipe, O_CLOEXEC) != 0 ||
|
||||
(options.captureOutput &&
|
||||
(pipe2(stdout_pipe, O_CLOEXEC) != 0 || pipe2(stderr_pipe, O_CLOEXEC) != 0)) ||
|
||||
(options.pipeStdin && pipe2(stdin_pipe, O_CLOEXEC) != 0)) {
|
||||
if (errorMessage) *errorMessage = std::strerror(errno);
|
||||
close_pipe(error_pipe); close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||
return {};
|
||||
}
|
||||
const pid_t child = fork();
|
||||
if (child < 0) {
|
||||
const std::string error = std::strerror(errno);
|
||||
close(error_pipe[0]);
|
||||
close(error_pipe[1]);
|
||||
return {false, 0, error};
|
||||
if (errorMessage) *errorMessage = std::strerror(errno);
|
||||
close_pipe(error_pipe); close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||
return {};
|
||||
}
|
||||
if (child == 0) {
|
||||
close(error_pipe[0]);
|
||||
if (options.detached) setsid();
|
||||
if (!options.workingDirectory.empty() && chdir(options.workingDirectory.c_str()) != 0) {
|
||||
const auto fail = [&](int code) {
|
||||
const int error = errno;
|
||||
write(error_pipe[1], &error, sizeof(error));
|
||||
_exit(126);
|
||||
_exit(code);
|
||||
};
|
||||
if (options.detached && setsid() < 0) fail(126);
|
||||
if (options.captureOutput) {
|
||||
if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0 ||
|
||||
dup2(stderr_pipe[1], STDERR_FILENO) < 0) fail(126);
|
||||
}
|
||||
if (options.pipeStdin && dup2(stdin_pipe[0], STDIN_FILENO) < 0) fail(126);
|
||||
close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||
if (!options.workingDirectory.empty() && chdir(options.workingDirectory.c_str()) != 0) {
|
||||
fail(126);
|
||||
}
|
||||
std::vector<std::string> values{options.executable.string()};
|
||||
values.insert(values.end(), options.arguments.begin(), options.arguments.end());
|
||||
@@ -158,26 +202,210 @@ ProcessResult LaunchProcess(const ProcessOptions& options) {
|
||||
for (auto& value : values) argv.push_back(value.data());
|
||||
argv.push_back(nullptr);
|
||||
execvp(argv[0], argv.data());
|
||||
const int error = errno;
|
||||
write(error_pipe[1], &error, sizeof(error));
|
||||
_exit(127);
|
||||
fail(127);
|
||||
}
|
||||
close(error_pipe[1]);
|
||||
error_pipe[1] = -1;
|
||||
if (stdout_pipe[1] >= 0) { close(stdout_pipe[1]); stdout_pipe[1] = -1; }
|
||||
if (stderr_pipe[1] >= 0) { close(stderr_pipe[1]); stderr_pipe[1] = -1; }
|
||||
if (stdin_pipe[0] >= 0) { close(stdin_pipe[0]); stdin_pipe[0] = -1; }
|
||||
int child_error = 0;
|
||||
const ssize_t error_bytes = read(error_pipe[0], &child_error, sizeof(child_error));
|
||||
close(error_pipe[0]);
|
||||
ssize_t error_bytes = -1;
|
||||
do { error_bytes = read(error_pipe[0], &child_error, sizeof(child_error)); }
|
||||
while (error_bytes < 0 && errno == EINTR);
|
||||
const int pipe_error = errno;
|
||||
close_pipe(error_pipe);
|
||||
if (error_bytes < 0) {
|
||||
kill(child, SIGKILL);
|
||||
int status = 0;
|
||||
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
||||
close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||
if (errorMessage) *errorMessage = std::strerror(pipe_error);
|
||||
return {};
|
||||
}
|
||||
if (error_bytes > 0) {
|
||||
int status = 0;
|
||||
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
||||
return {false, 0, std::strerror(child_error)};
|
||||
close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||
if (errorMessage) *errorMessage = std::strerror(child_error);
|
||||
return {};
|
||||
}
|
||||
std::thread([child] {
|
||||
auto state = std::make_shared<Process::State>();
|
||||
state->processId = child;
|
||||
state->stdinWrite = stdin_pipe[1];
|
||||
stdin_pipe[1] = -1;
|
||||
state->readers = options.captureOutput ? 2 : 0;
|
||||
const auto read_pipe = [](std::shared_ptr<Process::State> state, int pipe_value,
|
||||
bool standardError) {
|
||||
std::string output;
|
||||
char buffer[4096];
|
||||
ssize_t count = 0;
|
||||
while ((count = read(pipe_value, buffer, sizeof(buffer))) > 0)
|
||||
output.append(buffer, static_cast<std::size_t>(count));
|
||||
close(pipe_value);
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
(standardError ? state->stderrText : state->stdoutText) = std::move(output);
|
||||
--state->readers;
|
||||
state->changed.notify_all();
|
||||
};
|
||||
if (options.captureOutput) {
|
||||
std::thread(read_pipe, state, stdout_pipe[0], false).detach();
|
||||
std::thread(read_pipe, state, stderr_pipe[0], true).detach();
|
||||
stdout_pipe[0] = stderr_pipe[0] = -1;
|
||||
}
|
||||
std::thread([state] {
|
||||
int status = 0;
|
||||
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
||||
pid_t waited = -1;
|
||||
do { waited = waitpid(state->processId, &status, 0); } while (waited < 0 && errno == EINTR);
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (waited > 0 && WIFEXITED(status)) state->exitCode = WEXITSTATUS(status);
|
||||
else if (waited > 0 && WIFSIGNALED(status)) state->exitCode = 128 + WTERMSIG(status);
|
||||
state->finished = true;
|
||||
state->changed.notify_all();
|
||||
}).detach();
|
||||
return {true, static_cast<std::uint32_t>(child), {}};
|
||||
return Process(std::move(state));
|
||||
}
|
||||
|
||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
||||
std::string error;
|
||||
auto process = StartProcess(options, &error);
|
||||
return {static_cast<bool>(process), process.Id(), std::move(error)};
|
||||
}
|
||||
|
||||
std::uint32_t Process::Id() const noexcept {
|
||||
return state_ ? static_cast<std::uint32_t>(state_->processId) : 0;
|
||||
}
|
||||
|
||||
bool Process::IsRunning() const {
|
||||
if (!state_) return false;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return !state_->finished;
|
||||
}
|
||||
|
||||
bool Process::Wait(std::int64_t timeoutMs, std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state_->mutex);
|
||||
const auto done = [&] { return state_->finished && state_->readers == 0; };
|
||||
if (timeoutMs < 0) {
|
||||
state_->changed.wait(lock, done);
|
||||
return true;
|
||||
}
|
||||
return state_->changed.wait_for(lock, std::chrono::milliseconds(timeoutMs), done);
|
||||
}
|
||||
|
||||
bool Process::Terminate(std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
if (!IsRunning()) return true;
|
||||
if (kill(state_->processId, SIGKILL) == 0 || errno == ESRCH) return true;
|
||||
if (errorMessage) *errorMessage = std::strerror(errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<int> Process::ExitCode() const {
|
||||
if (!state_) return std::nullopt;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->finished ? std::optional<int>(state_->exitCode) : std::nullopt;
|
||||
}
|
||||
|
||||
bool Process::WriteStdin(std::string_view data, std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->stdinMutex);
|
||||
if (state_->stdinWrite < 0) {
|
||||
if (errorMessage) *errorMessage = "Process stdin is not piped";
|
||||
return false;
|
||||
}
|
||||
sigset_t blocked, previous, pending;
|
||||
sigemptyset(&blocked);
|
||||
sigaddset(&blocked, SIGPIPE);
|
||||
pthread_sigmask(SIG_BLOCK, &blocked, &previous);
|
||||
sigpending(&pending);
|
||||
const bool already_pending = sigismember(&pending, SIGPIPE) != 0;
|
||||
std::size_t written = 0;
|
||||
while (written < data.size()) {
|
||||
const ssize_t count = write(state_->stdinWrite, data.data() + written, data.size() - written);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count <= 0) {
|
||||
const int error = errno;
|
||||
if (error == EPIPE && !already_pending) {
|
||||
timespec timeout{};
|
||||
sigtimedwait(&blocked, nullptr, &timeout);
|
||||
}
|
||||
pthread_sigmask(SIG_SETMASK, &previous, nullptr);
|
||||
if (errorMessage) *errorMessage = std::strerror(error);
|
||||
return false;
|
||||
}
|
||||
written += static_cast<std::size_t>(count);
|
||||
}
|
||||
pthread_sigmask(SIG_SETMASK, &previous, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Process::CloseStdin(std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->stdinMutex);
|
||||
if (state_->stdinWrite < 0) return true;
|
||||
if (close(state_->stdinWrite) != 0) {
|
||||
if (errorMessage) *errorMessage = std::strerror(errno);
|
||||
return false;
|
||||
}
|
||||
state_->stdinWrite = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Process::Stdout() const {
|
||||
if (!state_) return {};
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->stdoutText;
|
||||
}
|
||||
|
||||
std::string Process::Stderr() const {
|
||||
if (!state_) return {};
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->stderrText;
|
||||
}
|
||||
|
||||
RunProcessResult RunProcess(const ProcessOptions& options) {
|
||||
RunProcessResult result;
|
||||
std::string error;
|
||||
auto process = StartProcess(options, &error);
|
||||
if (!process) {
|
||||
result.errorMessage = std::move(error);
|
||||
return result;
|
||||
}
|
||||
result.started = true;
|
||||
if (options.pipeStdin) process.CloseStdin();
|
||||
if (!process.Wait(options.timeoutMs, &result.errorMessage)) {
|
||||
result.timedOut = true;
|
||||
process.Terminate(&result.errorMessage);
|
||||
if (!process.Wait(1000) && result.errorMessage.empty())
|
||||
result.errorMessage = "Timed out while closing process pipes";
|
||||
}
|
||||
if (const auto code = process.ExitCode()) result.exitCode = *code;
|
||||
result.stdoutText = process.Stdout();
|
||||
result.stderrText = process.Stderr();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsProcessRunning(std::uint32_t processId) noexcept {
|
||||
if (!processId) return false;
|
||||
return kill(static_cast<pid_t>(processId), 0) == 0 || errno == EPERM;
|
||||
}
|
||||
|
||||
std::uint32_t GetCurrentProcessId() noexcept { return static_cast<std::uint32_t>(getpid()); }
|
||||
std::uint32_t GetParentProcessId() noexcept { return static_cast<std::uint32_t>(getppid()); }
|
||||
|
||||
DynamicLibrary::~DynamicLibrary() { Reset(); }
|
||||
DynamicLibrary::DynamicLibrary(DynamicLibrary&& other) noexcept : handle_(other.handle_) {
|
||||
other.handle_ = nullptr;
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -93,6 +99,7 @@ std::filesystem::path shell_folder(REFKNOWNFOLDERID id, std::string* errorMessag
|
||||
}
|
||||
|
||||
std::wstring quote(std::wstring value) {
|
||||
if (value.empty()) return L"\"\"";
|
||||
if (value.find_first_of(L" \t\"") == std::wstring::npos) return value;
|
||||
std::wstring result = L"\"";
|
||||
std::size_t slashes = 0;
|
||||
@@ -121,6 +128,25 @@ LONG WINAPI unhandled_exception(EXCEPTION_POINTERS* info) {
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Process::State {
|
||||
~State() {
|
||||
if (stdinWrite) CloseHandle(stdinWrite);
|
||||
if (process) CloseHandle(process);
|
||||
}
|
||||
|
||||
HANDLE process = nullptr;
|
||||
HANDLE stdinWrite = nullptr;
|
||||
std::uint32_t id = 0;
|
||||
mutable std::mutex mutex;
|
||||
std::mutex stdinMutex;
|
||||
std::condition_variable changed;
|
||||
bool finished = false;
|
||||
int readers = 0;
|
||||
int exitCode = -1;
|
||||
std::string stdoutText;
|
||||
std::string stderrText;
|
||||
};
|
||||
|
||||
std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||
switch (path) {
|
||||
case KnownPath::AppData: return shell_folder(FOLDERID_RoamingAppData, errorMessage);
|
||||
@@ -160,24 +186,239 @@ std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
||||
if (options.executable.empty()) return {false, 0, "Executable path is required"};
|
||||
Process StartProcess(const ProcessOptions& options, std::string* errorMessage) {
|
||||
if (options.executable.empty()) {
|
||||
if (errorMessage) *errorMessage = "Executable path is required";
|
||||
return {};
|
||||
}
|
||||
std::wstring command = quote(options.executable.wstring());
|
||||
for (const auto& argument : options.arguments) command += L' ' + quote(widen(argument));
|
||||
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
|
||||
HANDLE stdout_read = nullptr, stdout_write = nullptr;
|
||||
HANDLE stderr_read = nullptr, stderr_write = nullptr;
|
||||
HANDLE stdin_read = nullptr, stdin_write = nullptr;
|
||||
const auto close = [](HANDLE& handle) { if (handle) CloseHandle(handle); handle = nullptr; };
|
||||
if (options.captureOutput &&
|
||||
(!CreatePipe(&stdout_read, &stdout_write, &security, 0) ||
|
||||
!SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0) ||
|
||||
!CreatePipe(&stderr_read, &stderr_write, &security, 0) ||
|
||||
!SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0))) {
|
||||
if (errorMessage) *errorMessage = error_text("CreatePipe");
|
||||
close(stdout_read); close(stdout_write); close(stderr_read); close(stderr_write);
|
||||
return {};
|
||||
}
|
||||
if (options.pipeStdin &&
|
||||
(!CreatePipe(&stdin_read, &stdin_write, &security, 0) ||
|
||||
!SetHandleInformation(stdin_write, HANDLE_FLAG_INHERIT, 0))) {
|
||||
if (errorMessage) *errorMessage = error_text("CreatePipe");
|
||||
close(stdout_read); close(stdout_write); close(stderr_read); close(stderr_write);
|
||||
close(stdin_read); close(stdin_write);
|
||||
return {};
|
||||
}
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
if (options.captureOutput || options.pipeStdin) {
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdInput = options.pipeStdin ? stdin_read : GetStdHandle(STD_INPUT_HANDLE);
|
||||
startup.hStdOutput = options.captureOutput ? stdout_write : GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
startup.hStdError = options.captureOutput ? stderr_write : GetStdHandle(STD_ERROR_HANDLE);
|
||||
}
|
||||
PROCESS_INFORMATION process{};
|
||||
const auto workingDirectory = options.workingDirectory.wstring();
|
||||
DWORD flags = options.detached ? DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP : 0;
|
||||
if (!CreateProcessW(options.executable.c_str(), command.data(), nullptr, nullptr, FALSE, flags,
|
||||
if (!CreateProcessW(options.executable.c_str(), command.data(), nullptr, nullptr,
|
||||
options.captureOutput || options.pipeStdin, flags,
|
||||
nullptr, workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
&startup, &process)) {
|
||||
return {false, 0, error_text("CreateProcessW")};
|
||||
if (errorMessage) *errorMessage = error_text("CreateProcessW");
|
||||
close(stdout_read); close(stdout_write); close(stderr_read); close(stderr_write);
|
||||
close(stdin_read); close(stdin_write);
|
||||
return {};
|
||||
}
|
||||
const auto id = static_cast<std::uint32_t>(process.dwProcessId);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return {true, id, {}};
|
||||
close(stdout_write); close(stderr_write); close(stdin_read);
|
||||
auto state = std::make_shared<Process::State>();
|
||||
state->process = process.hProcess;
|
||||
state->stdinWrite = stdin_write;
|
||||
state->id = static_cast<std::uint32_t>(process.dwProcessId);
|
||||
state->readers = options.captureOutput ? 2 : 0;
|
||||
const auto read_pipe = [](std::shared_ptr<Process::State> state, HANDLE pipe,
|
||||
bool standardError) {
|
||||
std::string output;
|
||||
char buffer[4096];
|
||||
DWORD count = 0;
|
||||
while (ReadFile(pipe, buffer, sizeof(buffer), &count, nullptr) && count)
|
||||
output.append(buffer, count);
|
||||
CloseHandle(pipe);
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
(standardError ? state->stderrText : state->stdoutText) = std::move(output);
|
||||
--state->readers;
|
||||
state->changed.notify_all();
|
||||
};
|
||||
if (options.captureOutput) {
|
||||
std::thread(read_pipe, state, stdout_read, false).detach();
|
||||
std::thread(read_pipe, state, stderr_read, true).detach();
|
||||
}
|
||||
std::thread([state] {
|
||||
WaitForSingleObject(state->process, INFINITE);
|
||||
DWORD code = static_cast<DWORD>(-1);
|
||||
GetExitCodeProcess(state->process, &code);
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exitCode = static_cast<int>(code);
|
||||
state->finished = true;
|
||||
state->changed.notify_all();
|
||||
}).detach();
|
||||
return Process(std::move(state));
|
||||
}
|
||||
|
||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
||||
std::string error;
|
||||
auto process = StartProcess(options, &error);
|
||||
return {static_cast<bool>(process), process.Id(), std::move(error)};
|
||||
}
|
||||
|
||||
std::uint32_t Process::Id() const noexcept { return state_ ? state_->id : 0; }
|
||||
|
||||
bool Process::IsRunning() const {
|
||||
if (!state_) return false;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return !state_->finished;
|
||||
}
|
||||
|
||||
bool Process::Wait(std::int64_t timeoutMs, std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state_->mutex);
|
||||
const auto done = [&] { return state_->finished && state_->readers == 0; };
|
||||
if (timeoutMs < 0) {
|
||||
state_->changed.wait(lock, done);
|
||||
return true;
|
||||
}
|
||||
return state_->changed.wait_for(lock, std::chrono::milliseconds(timeoutMs), done);
|
||||
}
|
||||
|
||||
bool Process::Terminate(std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
if (!IsRunning()) return true;
|
||||
if (TerminateProcess(state_->process, 1)) return true;
|
||||
DWORD code = STILL_ACTIVE;
|
||||
if (GetExitCodeProcess(state_->process, &code) && code != STILL_ACTIVE) return true;
|
||||
if (errorMessage) *errorMessage = error_text("TerminateProcess");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<int> Process::ExitCode() const {
|
||||
if (!state_) return std::nullopt;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->finished ? std::optional<int>(state_->exitCode) : std::nullopt;
|
||||
}
|
||||
|
||||
bool Process::WriteStdin(std::string_view data, std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->stdinMutex);
|
||||
if (!state_->stdinWrite) {
|
||||
if (errorMessage) *errorMessage = "Process stdin is not piped";
|
||||
return false;
|
||||
}
|
||||
std::size_t written = 0;
|
||||
while (written < data.size()) {
|
||||
DWORD count = 0;
|
||||
const DWORD requested = static_cast<DWORD>((std::min)(
|
||||
data.size() - written, static_cast<std::size_t>((std::numeric_limits<DWORD>::max)())));
|
||||
if (!WriteFile(state_->stdinWrite, data.data() + written, requested, &count, nullptr)) {
|
||||
if (errorMessage) *errorMessage = error_text("WriteFile");
|
||||
return false;
|
||||
}
|
||||
written += count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Process::CloseStdin(std::string* errorMessage) {
|
||||
if (!state_) {
|
||||
if (errorMessage) *errorMessage = "Process is not valid";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->stdinMutex);
|
||||
if (!state_->stdinWrite) return true;
|
||||
if (!CloseHandle(state_->stdinWrite)) {
|
||||
if (errorMessage) *errorMessage = error_text("CloseHandle");
|
||||
return false;
|
||||
}
|
||||
state_->stdinWrite = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Process::Stdout() const {
|
||||
if (!state_) return {};
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->stdoutText;
|
||||
}
|
||||
|
||||
std::string Process::Stderr() const {
|
||||
if (!state_) return {};
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
return state_->stderrText;
|
||||
}
|
||||
|
||||
RunProcessResult RunProcess(const ProcessOptions& options) {
|
||||
RunProcessResult result;
|
||||
std::string error;
|
||||
auto process = StartProcess(options, &error);
|
||||
if (!process) {
|
||||
result.errorMessage = std::move(error);
|
||||
return result;
|
||||
}
|
||||
result.started = true;
|
||||
if (options.pipeStdin) process.CloseStdin();
|
||||
if (!process.Wait(options.timeoutMs, &result.errorMessage)) {
|
||||
result.timedOut = true;
|
||||
process.Terminate(&result.errorMessage);
|
||||
if (!process.Wait(1000) && result.errorMessage.empty())
|
||||
result.errorMessage = "Timed out while closing process pipes";
|
||||
}
|
||||
if (const auto code = process.ExitCode()) result.exitCode = *code;
|
||||
result.stdoutText = process.Stdout();
|
||||
result.stderrText = process.Stderr();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsProcessRunning(std::uint32_t processId) noexcept {
|
||||
if (!processId) return false;
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, processId);
|
||||
if (!process) return GetLastError() == ERROR_ACCESS_DENIED;
|
||||
const bool running = WaitForSingleObject(process, 0) == WAIT_TIMEOUT;
|
||||
CloseHandle(process);
|
||||
return running;
|
||||
}
|
||||
|
||||
std::uint32_t GetCurrentProcessId() noexcept { return ::GetCurrentProcessId(); }
|
||||
|
||||
std::uint32_t GetParentProcessId() noexcept {
|
||||
const DWORD current = ::GetCurrentProcessId();
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) return 0;
|
||||
PROCESSENTRY32W entry{};
|
||||
entry.dwSize = sizeof(entry);
|
||||
std::uint32_t parent = 0;
|
||||
if (Process32FirstW(snapshot, &entry)) {
|
||||
do {
|
||||
if (entry.th32ProcessID == current) {
|
||||
parent = entry.th32ParentProcessID;
|
||||
break;
|
||||
}
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
return parent;
|
||||
}
|
||||
|
||||
DynamicLibrary::~DynamicLibrary() { Reset(); }
|
||||
|
||||
15
src/process.cpp
Normal file
15
src/process.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include <izo/Process.hpp>
|
||||
|
||||
namespace izo {
|
||||
|
||||
Process::~Process() { CloseStdin(); }
|
||||
|
||||
Process& Process::operator=(Process&& other) noexcept {
|
||||
if (this != &other) {
|
||||
CloseStdin();
|
||||
state_ = std::move(other.state_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace izo
|
||||
Reference in New Issue
Block a user