VLink  2.0.0
A high-performance communication middleware
process.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file process.h
26  * @brief Portable child-process driver with asynchronous I/O notifications.
27  *
28  * @details
29  * @c vlink::Process spawns a single child program at a time and exposes a QProcess-style
30  * surface for inspecting its lifecycle, reading captured output, writing to its standard
31  * input, and triggering termination. Output capture, exit notification, and timer-driven
32  * polling all run on an internal monitor thread.
33  *
34  * Capability groups exposed by the class:
35  *
36  * | Group | Representative members |
37  * | --------------------- | ------------------------------------------------------------------------ |
38  * | Lifecycle | @c start, @c start_command, @c terminate, @c kill, @c close |
39  * | Process metadata | @c get_process_id, @c get_state, @c get_error, @c get_exit_code |
40  * | Environment & cwd | @c set_environment, @c set_inherit_environment, @c set_working_directory |
41  * | I/O routing | @c set_process_mode, @c get_process_mode |
42  * | stdout/stderr capture | @c read_line_stdout, @c read_all, @c bytes_available_stdout |
43  * | stdin writing | @c write, @c close_write_channel |
44  * | Async notifications | @c register_finished_callback, @c register_ready_read_stdout_callback |
45  * | Synchronous waits | @c wait_for_started, @c wait_for_finished, @c wait_for_ready_read |
46  * | Static helpers | @c execute, @c start_detached |
47  *
48  * I/O channel routing modes:
49  *
50  * | Mode | stdout | stderr |
51  * | ----------------------- | ------------------- | ------------------- |
52  * | @c kSeparateMode | Buffered pipe | Buffered pipe |
53  * | @c kMergedMode | Buffered pipe | Merged into stdout |
54  * | @c kForwardedMode | Inherits parent | Inherits parent |
55  * | @c kForwardedOutputMode | Inherits parent | Buffered pipe |
56  * | @c kForwardedErrorMode | Buffered pipe | Inherits parent |
57  *
58  * @note
59  * - Every callback fires from the monitor thread; protect shared caller state accordingly.
60  * - The destructor first sends @c SIGTERM, drains pending I/O, then optionally escalates to
61  * @c SIGKILL after @c kDestructorWaitTimeoutMs (5000 ms) before joining.
62  * - The class is non-copyable and non-movable.
63  *
64  * @par Example
65  * @code
66  * vlink::Process proc;
67  * proc.set_process_mode(vlink::Process::kSeparateMode);
68  * proc.register_ready_read_stdout_callback([&proc] {
69  * std::string line;
70  * while (proc.can_read_line_stdout()) {
71  * proc.read_line_stdout(line);
72  * }
73  * });
74  * proc.register_finished_callback([](int code, vlink::Process::ExitStatus status) {
75  * (void)code;
76  * (void)status;
77  * });
78  * proc.start("/usr/bin/ls", {"-la"});
79  * proc.wait_for_finished();
80  * @endcode
81  */
82 
83 #pragma once
84 
85 #include <cstdint>
86 #include <memory>
87 #include <string>
88 #include <unordered_map>
89 #include <vector>
90 
91 #include "./functional.h"
92 #include "./macros.h"
93 
94 namespace vlink {
95 
96 /**
97  * @class Process
98  * @brief Owns and drives a single child process with asynchronous I/O reporting.
99  *
100  * @details
101  * The instance is non-copyable and non-movable. A new child may only be launched after
102  * any previous one has terminated and the internal state has been cleaned up.
103  */
105  public:
106  /**
107  * @enum State
108  * @brief Coarse lifecycle stage of the child process.
109  */
110  enum State : uint8_t {
111  kNotRunningState = 0, ///< No child is currently active.
112  kStartingState = 1, ///< @c start() has been called and the OS is launching the binary.
113  kRunningState = 2, ///< Child has been successfully launched and is executing.
114  };
115 
116  /**
117  * @enum ExitStatus
118  * @brief How the most recent child exited.
119  */
120  enum ExitStatus : uint8_t {
121  kNormalExitStatus = 0, ///< Child returned from @c main or called @c exit normally.
122  kCrashExitStatus = 1, ///< Child was terminated by a signal or crashed.
123  };
124 
125  /**
126  * @enum Error
127  * @brief Sticky error indicator set by failing operations.
128  */
129  enum Error : uint8_t {
130  kNoError = 0, ///< No error has been observed.
131  kUnknownError = 1, ///< Catch-all bucket for unmapped errors.
132  kStartError = 2, ///< Failed to launch the requested program.
133  kCrashedError = 3, ///< Child process crashed.
134  kTimedOutError = 4, ///< A blocking wait expired before its condition was met.
135  kWriteError = 5, ///< Writing to the child's stdin failed.
136  kReadError = 6, ///< Reading from a captured pipe failed.
137  kBufferOverflowError = 7, ///< Captured output exceeded @c set_max_buffer_size.
138  };
139 
140  /**
141  * @enum Mode
142  * @brief Routing of the child's stdout/stderr file descriptors.
143  */
144  enum Mode : uint8_t {
145  kSeparateMode = 0, ///< Capture stdout and stderr through independent pipes.
146  kMergedMode = 1, ///< Capture stdout; merge stderr into the stdout pipe.
147  kForwardedMode = 2, ///< Inherit parent stdout/stderr without capture.
148  kForwardedOutputMode = 3, ///< Inherit stdout; capture stderr.
149  kForwardedErrorMode = 4, ///< Capture stdout; inherit stderr.
150  };
151 
152  /**
153  * @brief Map type used to pass environment overrides to @c set_environment().
154  */
155  using EnvironmentMap = std::unordered_map<std::string, std::string>;
156 
157  /**
158  * @brief Callback signature invoked when the @c Error state changes.
159  */
160  using ErrorCallback = Function<void(Error)>;
161 
162  /**
163  * @brief Callback signature invoked when the child exits.
164  *
165  * @details
166  * The first parameter is the exit code; the second is @c kNormalExitStatus or
167  * @c kCrashExitStatus.
168  */
169  using FinishedCallback = Function<void(int, ExitStatus)>;
170 
171  /**
172  * @brief Callback signature invoked whenever new data is available on a pipe.
173  */
174  using ReadyReadCallback = Function<void()>;
175 
176  /**
177  * @brief Callback signature invoked when the process @c State transitions.
178  */
180 
181  /**
182  * @brief Sentinel value used to request an unbounded wait.
183  */
184  static constexpr int kInfinite{-1};
185 
186  /**
187  * @brief Default deadline for @c wait_for_started / @c wait_for_finished (3000 ms).
188  */
189  static constexpr int kDefaultWaitTimeoutMs{3000};
190 
191  /**
192  * @brief Default deadline for blocking writes to the child's stdin (5000 ms).
193  */
194  static constexpr int kDefaultWriteTimeoutMs{5000};
195 
196  /**
197  * @brief Default deadline for the synchronous @c execute() helper (30000 ms).
198  */
199  static constexpr int kDefaultExecuteTimeoutMs{30000};
200 
201  /**
202  * @brief Grace period the destructor allows after sending @c SIGTERM before escalating.
203  */
204  static constexpr int kDestructorWaitTimeoutMs{5000};
205 
206  /**
207  * @brief Constructs an idle process driver with no child attached.
208  */
210 
211  /**
212  * @brief Destructor. Terminates the child if still alive then frees driver resources.
213  *
214  * @details
215  * The sequence sends @c SIGTERM via @c terminate(), drains any pending pipe data,
216  * waits up to @c kDestructorWaitTimeoutMs for a graceful exit, then escalates to
217  * @c SIGKILL via @c kill() and waits up to a further 1000 ms before releasing
218  * the I/O thread, pipes and child handles.
219  */
221 
222  Process(Process&& other) noexcept = delete;
223 
224  Process& operator=(Process&& other) noexcept = delete;
225 
226  /**
227  * @brief Returns the lifecycle state of the currently attached child.
228  *
229  * @return Current @c State value.
230  */
231  [[nodiscard]] State get_state() const;
232 
233  /**
234  * @brief Returns the most recently latched error code.
235  *
236  * @return Sticky @c Error value; @c kNoError if no error has occurred.
237  */
238  [[nodiscard]] Error get_error() const;
239 
240  /**
241  * @brief Returns the exit code of the most recent child process.
242  *
243  * @details
244  * Only meaningful once @c get_state() reports @c kNotRunningState.
245  *
246  * @return Exit code passed to @c exit() or implied by signal termination.
247  */
248  [[nodiscard]] int get_exit_code() const;
249 
250  /**
251  * @brief Returns how the most recent child terminated.
252  *
253  * @return @c kNormalExitStatus on graceful exit, @c kCrashExitStatus on signal/crash.
254  */
255  [[nodiscard]] ExitStatus get_exit_status() const;
256 
257  /**
258  * @brief Reports whether the attached child is currently running.
259  *
260  * @return @c true when @c get_state() equals @c kRunningState.
261  */
262  [[nodiscard]] bool is_running() const;
263 
264  /**
265  * @brief Returns the OS-level identifier of the child process.
266  *
267  * @return Process ID, or @c -1 when no child is attached.
268  */
269  [[nodiscard]] int64_t get_process_id() const;
270 
271  /**
272  * @brief Limits the in-memory capture buffer used for stdout/stderr.
273  *
274  * @details
275  * When the captured output exceeds @p size bytes the @c kBufferOverflowError code is
276  * latched. Passing @c 0 restores the default of 16 MiB.
277  *
278  * @param size New buffer limit in bytes.
279  */
280  void set_max_buffer_size(size_t size);
281 
282  /**
283  * @brief Returns the configured capture-buffer limit.
284  *
285  * @return Buffer size in bytes; default is 16 MiB.
286  */
287  [[nodiscard]] size_t get_max_buffer_size() const;
288 
289  /**
290  * @brief Defines the environment that will be applied at the next @c start() call.
291  *
292  * @param env_map Variable map to merge into or replace the parent environment based on
293  * the @c set_inherit_environment() setting.
294  */
295  void set_environment(const EnvironmentMap& env_map);
296 
297  /**
298  * @brief Returns the currently configured environment override.
299  *
300  * @return Environment variable map.
301  */
302  [[nodiscard]] EnvironmentMap get_environment() const;
303 
304  /**
305  * @brief Sets the I/O channel routing that will be applied at the next @c start() call.
306  *
307  * @param mode Routing mode for stdout/stderr.
308  */
309  void set_process_mode(Mode mode);
310 
311  /**
312  * @brief Returns the currently configured I/O routing mode.
313  *
314  * @return Current @c Mode.
315  */
316  [[nodiscard]] Mode get_process_mode() const;
317 
318  /**
319  * @brief Controls whether the child inherits the parent environment in addition to the override map.
320  *
321  * @param inherit @c true to merge with the parent environment, @c false to use only the override map.
322  */
323  void set_inherit_environment(bool inherit);
324 
325  /**
326  * @brief Reports whether the child currently inherits the parent environment.
327  *
328  * @return @c true when the parent environment is merged.
329  */
330  [[nodiscard]] bool get_inherit_environment() const;
331 
332  /**
333  * @brief Sets the working directory used at the next @c start() call.
334  *
335  * @param dir Absolute path the child should @c chdir to before executing the program.
336  */
337  void set_working_directory(const std::string& dir);
338 
339  /**
340  * @brief Returns the currently configured working directory.
341  *
342  * @return Working directory path.
343  */
344  [[nodiscard]] std::string get_working_directory() const;
345 
346  /**
347  * @brief Installs a callback fired when the @c Error state changes.
348  *
349  * @param callback Callback invoked from the monitor thread.
350  */
352 
353  /**
354  * @brief Installs a callback fired when the child exits.
355  *
356  * @param callback Callback invoked with exit code and exit status.
357  */
359 
360  /**
361  * @brief Installs a callback fired when stdout has new data buffered.
362  *
363  * @param callback Callback invoked from the monitor thread.
364  */
366 
367  /**
368  * @brief Installs a callback fired when stderr has new data buffered.
369  *
370  * @param callback Callback invoked from the monitor thread.
371  */
373 
374  /**
375  * @brief Installs a callback fired on every @c State transition.
376  *
377  * @param callback Callback invoked from the monitor thread with the new @c State.
378  */
380 
381  /**
382  * @brief Launches @p program with the given argument vector.
383  *
384  * @details
385  * Returns once the launch attempt has either succeeded (state becomes @c kRunningState)
386  * or failed (an @c Error is latched). I/O monitoring continues asynchronously on the
387  * internal monitor thread.
388  *
389  * @param program Path to the executable.
390  * @param arguments Argument vector excluding @c argv[0].
391  */
392  void start(const std::string& program, const std::vector<std::string>& arguments = {});
393 
394  /**
395  * @brief Parses a shell-style command line and launches it.
396  *
397  * @details
398  * Splits @p command on whitespace with quote and backslash handling, then delegates to
399  * @c start().
400  *
401  * @param command Shell-style command string.
402  */
403  void start_command(const std::string& command);
404 
405  /**
406  * @brief Blocks until the child leaves @c kStartingState or the timeout elapses.
407  *
408  * @details A child that already exited still counts as started.
409  *
410  * @param msecs Maximum wait in milliseconds. Default: @c kDefaultWaitTimeoutMs.
411  * @return @c true if the child started (even if since exited); @c false on launch
412  * failure, timeout, or if never started.
413  */
414  bool wait_for_started(int msecs = kDefaultWaitTimeoutMs);
415 
416  /**
417  * @brief Blocks until the child terminates or the timeout elapses.
418  *
419  * @param msecs Maximum wait in milliseconds. Default: @c kDefaultWaitTimeoutMs.
420  * @return @c true when the child has exited within the timeout.
421  */
422  bool wait_for_finished(int msecs = kDefaultWaitTimeoutMs);
423 
424  /**
425  * @brief Blocks until new pipe data becomes available or the timeout elapses.
426  *
427  * @param msecs Maximum wait in milliseconds. Default: @c kDefaultWaitTimeoutMs.
428  * @return @c true when data is now available on stdout or stderr.
429  */
430  bool wait_for_ready_read(int msecs = kDefaultWaitTimeoutMs);
431 
432  /**
433  * @brief Requests cooperative termination of the child.
434  *
435  * @details
436  * POSIX sends @c SIGTERM, which the child may catch. Windows uses @c TerminateProcess,
437  * which is unconditional. @c kill() must be used for a forced exit on POSIX.
438  */
439  void terminate();
440 
441  /**
442  * @brief Forces immediate termination of the child via @c SIGKILL / @c TerminateProcess.
443  */
444  void kill();
445 
446  /**
447  * @brief Sends @c SIGTERM and optionally escalates to @c kill() after the wait timeout.
448  *
449  * @param force_kill_on_timeout When @c true and the child has not exited within
450  * @c kDefaultWaitTimeoutMs, @c kill() is invoked. On
451  * Windows @c terminate() is already forceful so the flag
452  * is effectively ignored. Default: @c false.
453  */
454  void close(bool force_kill_on_timeout = false);
455 
456  /**
457  * @brief Returns the number of buffered bytes available to read from stdout.
458  *
459  * @return Bytes available on stdout.
460  */
461  [[nodiscard]] size_t bytes_available_stdout() const;
462 
463  /**
464  * @brief Returns the number of buffered bytes available to read from stderr.
465  *
466  * @return Bytes available on stderr.
467  */
468  [[nodiscard]] size_t bytes_available_stderr() const;
469 
470  /**
471  * @brief Reports whether a newline-terminated line is fully buffered on stdout.
472  *
473  * @return @c true when @c read_line_stdout() will deliver a complete line.
474  */
475  [[nodiscard]] bool can_read_line_stdout() const;
476 
477  /**
478  * @brief Reports whether a newline-terminated line is fully buffered on stderr.
479  *
480  * @return @c true when @c read_line_stderr() will deliver a complete line.
481  */
482  [[nodiscard]] bool can_read_line_stderr() const;
483 
484  /**
485  * @brief Reads one buffered line from stdout.
486  *
487  * @param line Destination string; receives buffered data including a trailing newline
488  * when one was buffered.
489  * @return @c true when at least one byte was copied into @p line.
490  */
491  bool read_line_stdout(std::string& line);
492 
493  /**
494  * @brief Reads one buffered line from stderr.
495  *
496  * @param line Destination string; receives buffered data including a trailing newline
497  * when one was buffered.
498  * @return @c true when at least one byte was copied into @p line.
499  */
500  bool read_line_stderr(std::string& line);
501 
502  /**
503  * @brief Reads up to @p max_size bytes of buffered stdout into @p buffer.
504  *
505  * @param buffer Destination byte vector.
506  * @param max_size Upper bound on bytes to copy.
507  * @return Number of bytes actually copied.
508  */
509  size_t read_stdout(std::vector<uint8_t>& buffer, size_t max_size);
510 
511  /**
512  * @brief Reads up to @p max_size bytes of buffered stderr into @p buffer.
513  *
514  * @param buffer Destination byte vector.
515  * @param max_size Upper bound on bytes to copy.
516  * @return Number of bytes actually copied.
517  */
518  size_t read_stderr(std::vector<uint8_t>& buffer, size_t max_size);
519 
520  /**
521  * @brief Drains all buffered stdout into @p buffer.
522  *
523  * @param buffer Destination byte vector; replaced.
524  * @return @c true when any data was copied.
525  */
526  bool read_all_output(std::vector<uint8_t>& buffer);
527 
528  /**
529  * @brief Drains all buffered stderr into @p buffer.
530  *
531  * @param buffer Destination byte vector; replaced.
532  * @return @c true when any data was copied.
533  */
534  bool read_all_error(std::vector<uint8_t>& buffer);
535 
536  /**
537  * @brief Drains all buffered stdout and stderr into @p buffer.
538  *
539  * @param buffer Destination byte vector; replaced.
540  * @return @c true when any data was copied.
541  */
542  bool read_all(std::vector<uint8_t>& buffer);
543 
544  /**
545  * @brief Drains all buffered stdout into @p str.
546  *
547  * @param str Destination string; replaced.
548  * @return @c true when any data was copied.
549  */
550  bool read_all_output(std::string& str);
551 
552  /**
553  * @brief Drains all buffered stderr into @p str.
554  *
555  * @param str Destination string; replaced.
556  * @return @c true when any data was copied.
557  */
558  bool read_all_error(std::string& str);
559 
560  /**
561  * @brief Drains all buffered stdout and stderr into @p str.
562  *
563  * @param str Destination string; replaced.
564  * @return @c true when any data was copied.
565  */
566  bool read_all(std::string& str);
567 
568  /**
569  * @brief Writes a byte buffer to the child's stdin.
570  *
571  * @param buffer Bytes to write.
572  * @param timeout_ms Maximum wait in milliseconds. Default: @c kDefaultWriteTimeoutMs.
573  * @return Number of bytes actually written.
574  */
575  size_t write(const std::vector<uint8_t>& buffer, int timeout_ms = kDefaultWriteTimeoutMs);
576 
577  /**
578  * @brief Writes a string to the child's stdin.
579  *
580  * @param str String to write.
581  * @param timeout_ms Maximum wait in milliseconds. Default: @c kDefaultWriteTimeoutMs.
582  * @return Number of bytes actually written.
583  */
584  size_t write(const std::string& str, int timeout_ms = kDefaultWriteTimeoutMs);
585 
586  /**
587  * @brief Closes the stdin pipe, signalling EOF to the child.
588  */
590 
591  /**
592  * @brief Synchronously executes a program and returns its exit code.
593  *
594  * @details
595  * Standard output and standard error are discarded. Returns @c -1 when the launch
596  * fails or the timeout elapses.
597  *
598  * @param program Path to the executable.
599  * @param arguments Argument vector. Default: empty.
600  * @param timeout_ms Maximum wait in milliseconds. Default: @c kDefaultExecuteTimeoutMs.
601  * @return Exit code on success, or @c -1 on failure.
602  */
603  static int execute(const std::string& program, const std::vector<std::string>& arguments = {},
604  int timeout_ms = kDefaultExecuteTimeoutMs);
605 
606  /**
607  * @brief Launches a program detached from the calling process and returns immediately.
608  *
609  * @param program Path to the executable.
610  * @param arguments Argument vector. Default: empty.
611  * @return @c true when the OS reports a successful launch.
612  */
613  static bool start_detached(const std::string& program, const std::vector<std::string>& arguments = {});
614 
615  private:
616  struct ReadResult final {
617  bool has_stdout_data{false};
618  bool has_stderr_data{false};
619  bool truncated{false};
620  bool read_error{false};
621  };
622 
623  void set_state(State state);
624 
625  void set_error(Error error);
626 
627  void cleanup();
628 
629  ReadResult read_from_pipes();
630 
631  void report_read_result(const ReadResult& result);
632 
633  void read_from_pipes_with_lock();
634 
635  bool setup_pipes();
636 
637  bool start_program(const std::string& program, const std::vector<std::string>& arguments);
638 
639  void monitor_thread();
640 
641  void start_monitor_thread();
642 
643  void stop_monitor_thread();
644 
645  void handle_process_exit(int exit_code, ExitStatus status);
646 
647  void invoke_callbacks_outside_lock(Error error_to_report, bool has_finished, int exit_code_to_report,
648  ExitStatus exit_status_to_report, State state_to_report, bool has_state_changed,
649  bool has_stdout_data, bool has_stderr_data);
650 
651  struct Impl;
652  std::unique_ptr<Impl> impl_;
653 
655 };
656 
657 } // namespace vlink
Pool-backed type-erased callables: copyable vlink::Function and move-only vlink::MoveFunction.
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VLINK_EXPORT
Definition: macros.h:81
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174