feat(process): add managed process control
This commit is contained in:
@@ -10,6 +10,7 @@ target_sources(izo
|
|||||||
src/command_line.cpp
|
src/command_line.cpp
|
||||||
src/dialogs.cpp
|
src/dialogs.cpp
|
||||||
src/directory_watcher.cpp
|
src/directory_watcher.cpp
|
||||||
|
src/process.cpp
|
||||||
src/time.cpp
|
src/time.cpp
|
||||||
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
|
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
|
||||||
$<$<PLATFORM_ID:Windows>:src/platform_windows.cpp>
|
$<$<PLATFORM_ID:Windows>:src/platform_windows.cpp>
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace izo {
|
namespace izo {
|
||||||
@@ -12,6 +16,10 @@ struct ProcessOptions {
|
|||||||
std::vector<std::string> arguments;
|
std::vector<std::string> arguments;
|
||||||
std::filesystem::path workingDirectory;
|
std::filesystem::path workingDirectory;
|
||||||
bool detached = false;
|
bool detached = false;
|
||||||
|
bool captureOutput = false;
|
||||||
|
bool pipeStdin = false;
|
||||||
|
// Used by RunProcess. A negative value waits indefinitely.
|
||||||
|
std::int64_t timeoutMs = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ProcessResult {
|
struct ProcessResult {
|
||||||
@@ -23,4 +31,55 @@ struct ProcessResult {
|
|||||||
|
|
||||||
ProcessResult LaunchProcess(const ProcessOptions& options);
|
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
|
} // namespace izo
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/sysinfo.h>
|
#include <sys/sysinfo.h>
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -103,6 +106,21 @@ std::string read_first_line(const std::filesystem::path& path) {
|
|||||||
|
|
||||||
} // namespace
|
} // 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) {
|
std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case KnownPath::AppData: return env_path("XDG_DATA_HOME", home() / ".local/share");
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
Process StartProcess(const ProcessOptions& options, std::string* errorMessage) {
|
||||||
if (options.executable.empty()) return {false, 0, "Executable path is required"};
|
if (options.executable.empty()) {
|
||||||
int error_pipe[2];
|
if (errorMessage) *errorMessage = "Executable path is required";
|
||||||
if (pipe2(error_pipe, O_CLOEXEC) != 0) return {false, 0, std::strerror(errno)};
|
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();
|
const pid_t child = fork();
|
||||||
if (child < 0) {
|
if (child < 0) {
|
||||||
const std::string error = std::strerror(errno);
|
if (errorMessage) *errorMessage = std::strerror(errno);
|
||||||
close(error_pipe[0]);
|
close_pipe(error_pipe); close_pipe(stdout_pipe); close_pipe(stderr_pipe); close_pipe(stdin_pipe);
|
||||||
close(error_pipe[1]);
|
return {};
|
||||||
return {false, 0, error};
|
|
||||||
}
|
}
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
close(error_pipe[0]);
|
close(error_pipe[0]);
|
||||||
if (options.detached) setsid();
|
const auto fail = [&](int code) {
|
||||||
if (!options.workingDirectory.empty() && chdir(options.workingDirectory.c_str()) != 0) {
|
|
||||||
const int error = errno;
|
const int error = errno;
|
||||||
write(error_pipe[1], &error, sizeof(error));
|
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()};
|
std::vector<std::string> values{options.executable.string()};
|
||||||
values.insert(values.end(), options.arguments.begin(), options.arguments.end());
|
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());
|
for (auto& value : values) argv.push_back(value.data());
|
||||||
argv.push_back(nullptr);
|
argv.push_back(nullptr);
|
||||||
execvp(argv[0], argv.data());
|
execvp(argv[0], argv.data());
|
||||||
const int error = errno;
|
fail(127);
|
||||||
write(error_pipe[1], &error, sizeof(error));
|
|
||||||
_exit(127);
|
|
||||||
}
|
}
|
||||||
close(error_pipe[1]);
|
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;
|
int child_error = 0;
|
||||||
const ssize_t error_bytes = read(error_pipe[0], &child_error, sizeof(child_error));
|
ssize_t error_bytes = -1;
|
||||||
close(error_pipe[0]);
|
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) {
|
if (error_bytes > 0) {
|
||||||
int status = 0;
|
int status = 0;
|
||||||
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
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;
|
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();
|
}).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() { Reset(); }
|
||||||
DynamicLibrary::DynamicLibrary(DynamicLibrary&& other) noexcept : handle_(other.handle_) {
|
DynamicLibrary::DynamicLibrary(DynamicLibrary&& other) noexcept : handle_(other.handle_) {
|
||||||
other.handle_ = nullptr;
|
other.handle_ = nullptr;
|
||||||
|
|||||||
@@ -7,9 +7,15 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
|
#include <tlhelp32.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -93,6 +99,7 @@ std::filesystem::path shell_folder(REFKNOWNFOLDERID id, std::string* errorMessag
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::wstring quote(std::wstring value) {
|
std::wstring quote(std::wstring value) {
|
||||||
|
if (value.empty()) return L"\"\"";
|
||||||
if (value.find_first_of(L" \t\"") == std::wstring::npos) return value;
|
if (value.find_first_of(L" \t\"") == std::wstring::npos) return value;
|
||||||
std::wstring result = L"\"";
|
std::wstring result = L"\"";
|
||||||
std::size_t slashes = 0;
|
std::size_t slashes = 0;
|
||||||
@@ -121,6 +128,25 @@ LONG WINAPI unhandled_exception(EXCEPTION_POINTERS* info) {
|
|||||||
|
|
||||||
} // namespace
|
} // 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) {
|
std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case KnownPath::AppData: return shell_folder(FOLDERID_RoamingAppData, errorMessage);
|
case KnownPath::AppData: return shell_folder(FOLDERID_RoamingAppData, errorMessage);
|
||||||
@@ -160,24 +186,239 @@ std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessResult LaunchProcess(const ProcessOptions& options) {
|
Process StartProcess(const ProcessOptions& options, std::string* errorMessage) {
|
||||||
if (options.executable.empty()) return {false, 0, "Executable path is required"};
|
if (options.executable.empty()) {
|
||||||
|
if (errorMessage) *errorMessage = "Executable path is required";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
std::wstring command = quote(options.executable.wstring());
|
std::wstring command = quote(options.executable.wstring());
|
||||||
for (const auto& argument : options.arguments) command += L' ' + quote(widen(argument));
|
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{};
|
STARTUPINFOW startup{};
|
||||||
startup.cb = sizeof(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{};
|
PROCESS_INFORMATION process{};
|
||||||
const auto workingDirectory = options.workingDirectory.wstring();
|
const auto workingDirectory = options.workingDirectory.wstring();
|
||||||
DWORD flags = options.detached ? DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP : 0;
|
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(),
|
nullptr, workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||||
&startup, &process)) {
|
&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.hThread);
|
||||||
CloseHandle(process.hProcess);
|
close(stdout_write); close(stderr_write); close(stdin_read);
|
||||||
return {true, id, {}};
|
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(); }
|
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