aboutsummaryrefslogtreecommitdiff
path: root/include/cru/base/SubProcess.h
diff options
context:
space:
mode:
Diffstat (limited to 'include/cru/base/SubProcess.h')
-rw-r--r--include/cru/base/SubProcess.h258
1 files changed, 258 insertions, 0 deletions
diff --git a/include/cru/base/SubProcess.h b/include/cru/base/SubProcess.h
new file mode 100644
index 00000000..fbe8ad2b
--- /dev/null
+++ b/include/cru/base/SubProcess.h
@@ -0,0 +1,258 @@
+#pragma once
+#include "Base.h"
+#include "Exception.h"
+#include "String.h"
+#include "io/Stream.h"
+
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <optional>
+#include <unordered_map>
+#include <vector>
+
+namespace cru {
+enum class SubProcessStatus {
+ /**
+ * @brief The process has not been created and started.
+ */
+ Prepare,
+ /**
+ * @brief The process is failed to start.
+ */
+ FailedToStart,
+ /**
+ * @brief The process is running now.
+ */
+ Running,
+ /**
+ * @brief The process has been exited.
+ */
+ Exited,
+};
+
+class CRU_BASE_API SubProcessException : public Exception {
+ public:
+ using Exception::Exception;
+};
+
+class CRU_BASE_API SubProcessFailedToStartException
+ : public SubProcessException {
+ public:
+ using SubProcessException::SubProcessException;
+};
+
+class CRU_BASE_API SubProcessInternalException : public SubProcessException {
+ public:
+ using SubProcessException::SubProcessException;
+};
+
+struct SubProcessStartInfo {
+ String program;
+ std::vector<String> arguments;
+ std::unordered_map<String, String> environments;
+};
+
+enum class SubProcessExitType {
+ Unknown,
+ Normal,
+ Signal,
+};
+
+struct SubProcessExitResult {
+ SubProcessExitType exit_type;
+ int exit_code;
+ int exit_signal;
+ bool has_core_dump;
+
+ bool IsSuccess() const {
+ return exit_type == SubProcessExitType::Normal && exit_code == 0;
+ }
+
+ static SubProcessExitResult Unknown() {
+ return {SubProcessExitType::Unknown, 0, 0, false};
+ }
+
+ static SubProcessExitResult Normal(int exit_code) {
+ return {SubProcessExitType::Normal, exit_code, 0, false};
+ }
+
+ static SubProcessExitResult Signal(int exit_signal, bool has_core_dump) {
+ return {SubProcessExitType::Normal, 0, exit_signal, has_core_dump};
+ }
+};
+
+struct IPlatformSubProcessImpl : virtual Interface {
+ /**
+ * @brief Create and start a real process.
+ *
+ * If the program can't be created or start, an exception should be
+ * thrown.
+ *
+ * Implementation should fill internal data of the new process and start
+ * it.
+ *
+ * This method will be called only once in first call of `Start` on this
+ * thread with lock holdDefaultConstructible.
+ */
+ virtual void PlatformCreateProcess(const SubProcessStartInfo& start_info) = 0;
+
+ /**
+ * @brief Wait for the created process forever and return the exit result
+ * when process stops.
+ *
+ * Implementation should wait for the real process forever, after that,
+ * fill internal data and returns exit result.
+ *
+ * This method will be called only once on another thread after
+ * `PlatformCreateProcess` returns successfully
+ */
+ virtual SubProcessExitResult PlatformWaitForProcess() = 0;
+
+ /**
+ * @brief Kill the process immediately.
+ *
+ * This method will be called only once on this thread given
+ * `PlatformCreateProcess` returns successfully. There will be a window
+ * that the window already exits but the status has not been updated and
+ * this is called. So handle this gracefully and write to internal data
+ * carefully.
+ */
+ virtual void PlatformKillProcess() = 0;
+
+ virtual io::Stream* GetStdinStream() = 0;
+ virtual io::Stream* GetStdoutStream() = 0;
+ virtual io::Stream* GetStderrStream() = 0;
+};
+
+/**
+ * @brief A wrapper platform process. It is one-time, which means it
+ * starts and exits and can't start again.
+ *
+ * TODO: Current implementation has a problem. If the process does not exit for
+ * a long time, the resource related to it will not be released. It may cause a
+ * leak.
+ */
+class PlatformSubProcess : public Object {
+ CRU_DEFINE_CLASS_LOG_TAG(u"PlatformSubProcess")
+
+ private:
+ struct State {
+ explicit State(SubProcessStartInfo start_info,
+ std::shared_ptr<IPlatformSubProcessImpl> impl)
+ : start_info(std::move(start_info)), impl(std::move(impl)) {}
+
+ std::mutex mutex;
+ std::condition_variable condition_variable;
+ SubProcessStartInfo start_info;
+ SubProcessExitResult exit_result;
+ SubProcessStatus status = SubProcessStatus::Prepare;
+ bool killed = false;
+ std::shared_ptr<IPlatformSubProcessImpl> impl;
+ };
+
+ public:
+ PlatformSubProcess(SubProcessStartInfo start_info,
+ std::shared_ptr<IPlatformSubProcessImpl> impl);
+
+ ~PlatformSubProcess() override;
+
+ /**
+ * @brief Create and start a real process. If the process can't be created or
+ * start, `SubProcessFailedToStartException` will be thrown. If this function
+ * is already called once, `SubProcessException` will be thrown. Ensure only
+ * call this method once.
+ */
+ void Start();
+
+ /**
+ * @brief Wait for the process to exit optionally for at most `wait_time`. If
+ * the process already exits, it will return immediately. If the process has
+ * not started or failed to start, `SubProcessException` will be thrown.
+ * Ensure `Start` is called and does not throw before calling this.
+ *
+ * @remarks You may wish this returns bool to indicate whether it is timeout
+ * or the process exits. But no, even if it is timeout, the process may also
+ * have exited due to task schedule.
+ */
+ void Wait(std::optional<std::chrono::milliseconds> wait_time);
+
+ /**
+ * @brief kill the process if it is running. If the process already exits,
+ * nothing will happen. If the process has not started or failed to start,
+ * `SubProcessException` will be throw. Ensure `Start` is called and does not
+ * throw before calling this.
+ */
+ void Kill();
+
+ /**
+ * @brief Get the status of the process.
+ * 1. If the process has tried to start, aka `Start` is called, then this
+ * method will return one of `Running`, `FailedToStart`, `Exited`.
+ * 2. If it returns `Prepare`, `Start` is not called.
+ * 3. It does NOT guarantee that this return `Running` and the process is
+ * actually running. Because there might be a window that the process exits
+ * already but status is not updated.
+ */
+ SubProcessStatus GetStatus();
+
+ /**
+ * @brief Get the exit result. If the process is not started, failed to start
+ * or running, `SubProcessException` will be thrown.
+ */
+ SubProcessExitResult GetExitResult();
+
+ io::Stream* GetStdinStream();
+ io::Stream* GetStdoutStream();
+ io::Stream* GetStderrStream();
+
+ private:
+ std::shared_ptr<State> state_;
+ std::unique_lock<std::mutex> lock_;
+};
+
+class CRU_BASE_API SubProcess : public Object {
+ CRU_DEFINE_CLASS_LOG_TAG(u"SubProcess")
+
+ public:
+ static SubProcess Create(
+ String program, std::vector<String> arguments = {},
+ std::unordered_map<String, String> environments = {});
+
+ static SubProcessExitResult Call(
+ String program, std::vector<String> arguments = {},
+ std::unordered_map<String, String> environments = {});
+
+ public:
+ SubProcess(SubProcessStartInfo start_info);
+
+ CRU_DELETE_COPY(SubProcess)
+
+ SubProcess(SubProcess&& other) = default;
+ SubProcess& operator=(SubProcess&& other) = default;
+
+ ~SubProcess() override;
+
+ public:
+ void Wait(std::optional<std::chrono::milliseconds> wait_time = std::nullopt);
+ void Kill();
+
+ SubProcessStatus GetStatus();
+ SubProcessExitResult GetExitResult();
+
+ io::Stream* GetStdinStream();
+ io::Stream* GetStdoutStream();
+ io::Stream* GetStderrStream();
+
+ void Detach();
+
+ bool IsValid() const { return platform_process_ != nullptr; }
+ explicit operator bool() const { return IsValid(); }
+
+ private:
+ void CheckValid() const;
+
+ private:
+ std::unique_ptr<PlatformSubProcess> platform_process_;
+};
+} // namespace cru