VLink 2.0.0
A high-performance communication middleware
Loading...
Searching...
No Matches
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 Cross-platform child process management with I/O piping and async callbacks.
27 *
28 * @details
29 * @c Process provides a Qt-QProcess-inspired API for launching, monitoring and communicating
30 * with child processes. It supports:
31 * - Asynchronous state change, I/O ready-read and exit callbacks.
32 * - Multiple channel modes (separate stdout/stderr, merged, or forwarded to parent).
33 * - Line-by-line and bulk I/O reading from stdout and stderr.
34 * - Writing to stdin with a configurable timeout.
35 * - Synchronous helpers (@c execute, @c start_detached) for fire-and-forget use cases.
36 *
37 * Channel modes:
38 *
39 * | Mode | stdout | stderr |
40 * | ---------------------- | ------------------- | ------------------- |
41 * | @c kSeparateMode | Buffered pipe | Buffered pipe |
42 * | @c kMergedMode | Buffered pipe | Merged into stdout |
43 * | @c kForwardedMode | Forwarded to parent | Forwarded to parent |
44 * | @c kForwardedOutputMode| Forwarded to parent | Buffered pipe |
45 * | @c kForwardedErrorMode | Buffered pipe | Forwarded to parent |
46 *
47 * @note
48 * - All callbacks are invoked from an internal monitor thread; access shared state with care.
49 * - @c close() requests termination and optionally force-kills after a timeout.
50 * - @c Process objects are non-moveable and non-copyable.
51 * - The destructor waits @c kDestructorWaitTimeoutMs (5 s) for the process to exit.
52 *
53 * @par Example
54 * @code
55 * vlink::Process proc;
56 * proc.set_process_mode(vlink::Process::kSeparateMode);
57 * proc.register_ready_read_stdout_callback([&proc]() {
58 * std::string line;
59 * while (proc.can_read_line_stdout()) {
60 * proc.read_line_stdout(line);
61 * // handle line
62 * }
63 * });
64 * proc.register_finished_callback([](int code, vlink::Process::ExitStatus status) {
65 * // handle exit
66 * });
67 * proc.start("/usr/bin/ls", {"-la"});
68 * proc.wait_for_finished();
69 * @endcode
70 */
71
72#pragma once
73
74#include <cstdint>
75#include <functional>
76#include <memory>
77#include <string>
78#include <unordered_map>
79#include <vector>
80
81#include "./macros.h"
82
83namespace vlink {
84
85/**
86 * @class Process
87 * @brief Cross-platform child process with async I/O and state notification.
88 *
89 * @details
90 * A single @c Process object manages one child process at a time.
91 * It is non-copyable and non-moveable.
92 */
94 public:
95 /**
96 * @brief Lifecycle state of the child process.
97 */
98 enum State : uint8_t {
99 kNotRunningState = 0, ///< Not started or has exited
100 kStartingState = 1, ///< @c start() called; waiting for exec to complete
101 kRunningState = 2, ///< Successfully running
102 };
103
104 /**
105 * @brief How the child process exited.
106 */
107 enum ExitStatus : uint8_t {
108 kNormalExitStatus = 0, ///< Exited normally (via exit() or return from main)
109 kCrashExitStatus = 1, ///< Killed by a signal or crashed
110 };
111
112 /**
113 * @brief Error codes set on failure.
114 */
115 enum Error : uint8_t {
116 kNoError = 0, ///< No error
117 kUnknownError = 1, ///< Unknown error
118 kStartError = 2, ///< Failed to start the process (e.g., file not found)
119 kCrashedError = 3, ///< Process crashed
120 kTimedOutError = 4, ///< A wait operation timed out
121 kWriteError = 5, ///< Write to stdin failed
122 kReadError = 6, ///< Read from stdout/stderr failed
123 kBufferOverflowError = 7, ///< Output exceeded @c max_buffer_size
124 };
125
126 /**
127 * @brief I/O channel routing mode.
128 */
129 enum Mode : uint8_t {
130 kSeparateMode = 0, ///< stdout and stderr buffered as separate pipes
131 kMergedMode = 1, ///< stderr merged into stdout pipe
132 kForwardedMode = 2, ///< Both stdout and stderr forwarded to the parent process
133 kForwardedOutputMode = 3, ///< stdout forwarded; stderr buffered separately
134 kForwardedErrorMode = 4, ///< stderr forwarded; stdout buffered separately
135 };
136
137 /**
138 * @brief Environment variable map type for @c set_environment().
139 */
140 using EnvironmentMap = std::unordered_map<std::string, std::string>;
141
142 /**
143 * @brief Callback type invoked when an error occurs.
144 */
145 using ErrorCallback = std::function<void(Error)>;
146
147 /**
148 * @brief Callback type invoked when the process exits.
149 *
150 * @details
151 * First argument is the exit code; second is @c kNormalExitStatus or @c kCrashExitStatus.
152 */
153 using FinishedCallback = std::function<void(int, ExitStatus)>;
154
155 /**
156 * @brief Callback type invoked when new data is available on a pipe.
157 */
158 using ReadyReadCallback = std::function<void()>;
159
160 /**
161 * @brief Callback type invoked when the process state changes.
162 */
163 using StateChangedCallback = std::function<void(State)>;
164
165 /**
166 * @brief Sentinel wait timeout meaning wait indefinitely.
167 */
168 static constexpr int kInfinite{-1};
169
170 /**
171 * @brief Default timeout for @c wait_for_started() and @c wait_for_finished() in milliseconds.
172 */
173 static constexpr int kDefaultWaitTimeoutMs{3000};
174
175 /**
176 * @brief Default timeout for @c write() in milliseconds.
177 */
178 static constexpr int kDefaultWriteTimeoutMs{5000};
179
180 /**
181 * @brief Default timeout for the synchronous @c execute() helper in milliseconds.
182 */
183 static constexpr int kDefaultExecuteTimeoutMs{30000};
184
185 /**
186 * @brief Time the destructor waits for the child to exit before force-killing in milliseconds.
187 */
188 static constexpr int kDestructorWaitTimeoutMs{5000};
189
190 /**
191 * @brief Constructs a @c Process object. No child is started yet.
192 */
194
195 /**
196 * @brief Destructor. Closes the process and waits up to @c kDestructorWaitTimeoutMs.
197 */
199
200 Process(Process&& other) noexcept = delete;
201
202 Process& operator=(Process&& other) noexcept = delete;
203
204 /**
205 * @brief Returns the current state of the child process.
206 *
207 * @return Current @c State value.
208 */
209 [[nodiscard]] State get_state() const;
210
211 /**
212 * @brief Returns the last error code.
213 *
214 * @return @c kNoError if no error has occurred.
215 */
216 [[nodiscard]] Error get_error() const;
217
218 /**
219 * @brief Returns the exit code of the child process.
220 *
221 * @details
222 * Valid only after @c get_state() returns @c kNotRunningState.
223 *
224 * @return Exit code.
225 */
226 [[nodiscard]] int get_exit_code() const;
227
228 /**
229 * @brief Returns how the child process exited.
230 *
231 * @return @c kNormalExitStatus or @c kCrashExitStatus.
232 */
233 [[nodiscard]] ExitStatus get_exit_status() const;
234
235 /**
236 * @brief Returns @c true if the child process is currently running.
237 *
238 * @return @c true if state is @c kRunningState.
239 */
240 [[nodiscard]] bool is_running() const;
241
242 /**
243 * @brief Returns the operating-system process ID of the child.
244 *
245 * @return PID, or -1 if not running.
246 */
247 [[nodiscard]] int64_t get_process_id() const;
248
249 /**
250 * @brief Sets the maximum buffer size for stdout and stderr capture.
251 *
252 * @details
253 * If the child produces more output than @p size bytes, @c kBufferOverflowError is set.
254 * Passing 0 resets the limit to the default (16 MB).
255 *
256 * @param size Maximum buffer size in bytes.
257 */
258 void set_max_buffer_size(size_t size);
259
260 /**
261 * @brief Returns the configured maximum buffer size.
262 *
263 * @return Buffer size in bytes. Default is 16 MB (16 * 1024 * 1024).
264 */
265 [[nodiscard]] size_t get_max_buffer_size() const;
266
267 /**
268 * @brief Sets the environment variables for the child process.
269 *
270 * @details
271 * Replaces (or supplements, depending on @c set_inherit_environment) the child's
272 * environment with @p env_map.
273 *
274 * @param env_map Map of variable name to value.
275 */
276 void set_environment(const EnvironmentMap& env_map);
277
278 /**
279 * @brief Returns the configured environment map.
280 *
281 * @return Environment variable map.
282 */
283 [[nodiscard]] EnvironmentMap get_environment() const;
284
285 /**
286 * @brief Sets the I/O channel routing mode.
287 *
288 * @param mode Channel mode. Must be set before @c start().
289 */
291
292 /**
293 * @brief Returns the configured I/O channel mode.
294 *
295 * @return Current mode.
296 */
297 [[nodiscard]] Mode get_process_mode() const;
298
299 /**
300 * @brief Controls whether the child inherits the parent's environment.
301 *
302 * @param inherit If @c true, the child inherits all parent environment variables.
303 * If @c false, only the variables in the @c EnvironmentMap are set.
304 */
305 void set_inherit_environment(bool inherit);
306
307 /**
308 * @brief Returns whether the child inherits the parent environment.
309 *
310 * @return @c true if inheriting.
311 */
312 [[nodiscard]] bool get_inherit_environment() const;
313
314 /**
315 * @brief Sets the working directory for the child process.
316 *
317 * @param dir Absolute path to the working directory.
318 */
319 void set_working_directory(const std::string& dir);
320
321 /**
322 * @brief Returns the configured working directory.
323 *
324 * @return Working directory path.
325 */
326 [[nodiscard]] std::string get_working_directory() const;
327
328 /**
329 * @brief Registers a callback for error events.
330 *
331 * @param callback Invoked with the @c Error code when an error occurs.
332 */
334
335 /**
336 * @brief Registers a callback invoked when the child exits.
337 *
338 * @param callback Invoked with @c (exit_code, exit_status).
339 */
341
342 /**
343 * @brief Registers a callback invoked when new stdout data is available.
344 *
345 * @param callback Invoked from the monitor thread when stdout has data.
346 */
348
349 /**
350 * @brief Registers a callback invoked when new stderr data is available.
351 *
352 * @param callback Invoked from the monitor thread when stderr has data.
353 */
355
356 /**
357 * @brief Registers a callback invoked when the process state changes.
358 *
359 * @param callback Invoked with the new @c State value.
360 */
362
363 /**
364 * @brief Launches the child process.
365 *
366 * @details
367 * The child is started asynchronously. Call @c wait_for_started() to block until
368 * the child is in @c kRunningState.
369 *
370 * @param program Path to the executable.
371 * @param arguments Command-line arguments. Default: empty.
372 */
373 void start(const std::string& program, const std::vector<std::string>& arguments = {});
374
375 /**
376 * @brief Parses and launches a shell command string.
377 *
378 * @details
379 * Splits @p command by whitespace into a program and argument list, then calls @c start().
380 *
381 * @param command Shell command string.
382 */
383 void start_command(const std::string& command);
384
385 /**
386 * @brief Blocks until the child process enters @c kRunningState.
387 *
388 * @param msecs Timeout in milliseconds. Default: @c kDefaultWaitTimeoutMs (3000 ms).
389 * @return @c true if the process is running within the timeout.
390 */
392
393 /**
394 * @brief Blocks until the child process exits.
395 *
396 * @param msecs Timeout in milliseconds. Default: @c kDefaultWaitTimeoutMs (3000 ms).
397 * @return @c true if the process exited within the timeout.
398 */
400
401 /**
402 * @brief Blocks until new data is available on any pipe.
403 *
404 * @param msecs Timeout in milliseconds. Default: @c kDefaultWaitTimeoutMs (3000 ms).
405 * @return @c true if data became available within the timeout.
406 */
408
409 /**
410 * @brief Requests child termination.
411 *
412 * @details
413 * On POSIX this sends @c SIGTERM, which the child may handle or ignore.
414 * On Windows this uses @c TerminateProcess, so termination is immediate.
415 * Use @c kill() for an explicit forceful stop.
416 */
417 void terminate();
418
419 /**
420 * @brief Forcefully kills the child process (SIGKILL / TerminateProcess).
421 */
422 void kill();
423
424 /**
425 * @brief Calls @c terminate() and optionally force-kills after a timeout.
426 *
427 * @param force_kill_on_timeout If @c true, calls @c kill() if the process has not exited
428 * after @c kDestructorWaitTimeoutMs. If @c false and the child
429 * does not exit in time, the process remains running. On Windows,
430 * @c terminate() is already forceful. Default: @c false.
431 */
432 void close(bool force_kill_on_timeout = false);
433
434 /**
435 * @brief Returns the number of bytes available to read from stdout.
436 *
437 * @return Bytes available in the stdout buffer.
438 */
439 [[nodiscard]] size_t bytes_available_stdout() const;
440
441 /**
442 * @brief Returns the number of bytes available to read from stderr.
443 *
444 * @return Bytes available in the stderr buffer.
445 */
446 [[nodiscard]] size_t bytes_available_stderr() const;
447
448 /**
449 * @brief Returns @c true if a complete newline-terminated line is available on stdout.
450 *
451 * @return @c true if at least one line can be read.
452 */
453 [[nodiscard]] bool can_read_line_stdout() const;
454
455 /**
456 * @brief Returns @c true if a complete newline-terminated line is available on stderr.
457 *
458 * @return @c true if at least one line can be read.
459 */
460 [[nodiscard]] bool can_read_line_stderr() const;
461
462 /**
463 * @brief Reads one line from stdout into @p line.
464 *
465 * @param line Output string. Includes the trailing newline when one is buffered.
466 * @return @c true if a line was read.
467 */
468 bool read_line_stdout(std::string& line);
469
470 /**
471 * @brief Reads one line from stderr into @p line.
472 *
473 * @param line Output string. Includes the trailing newline when one is buffered.
474 * @return @c true if a line was read.
475 */
476 bool read_line_stderr(std::string& line);
477
478 /**
479 * @brief Reads up to @p max_size bytes from stdout into @p buffer.
480 *
481 * @param buffer Destination vector.
482 * @param max_size Maximum bytes to read.
483 * @return Number of bytes actually read.
484 */
485 size_t read_stdout(std::vector<uint8_t>& buffer, size_t max_size);
486
487 /**
488 * @brief Reads up to @p max_size bytes from stderr into @p buffer.
489 *
490 * @param buffer Destination vector.
491 * @param max_size Maximum bytes to read.
492 * @return Number of bytes actually read.
493 */
494 size_t read_stderr(std::vector<uint8_t>& buffer, size_t max_size);
495
496 /**
497 * @brief Reads all available stdout data into @p buffer (byte vector overload).
498 *
499 * @param buffer Destination vector; existing content is replaced.
500 * @return @c true if any data was available.
501 */
502 bool read_all_output(std::vector<uint8_t>& buffer);
503
504 /**
505 * @brief Reads all available stderr data into @p buffer (byte vector overload).
506 *
507 * @param buffer Destination vector; existing content is replaced.
508 * @return @c true if any data was available.
509 */
510 bool read_all_error(std::vector<uint8_t>& buffer);
511
512 /**
513 * @brief Reads all available stdout and stderr data into @p buffer (byte vector overload).
514 *
515 * @param buffer Destination vector; existing content is replaced.
516 * @return @c true if any data was available.
517 */
518 bool read_all(std::vector<uint8_t>& buffer);
519
520 /**
521 * @brief Reads all available stdout data into @p str.
522 *
523 * @param str Destination string.
524 * @return @c true if any data was available.
525 */
526 bool read_all_output(std::string& str);
527
528 /**
529 * @brief Reads all available stderr data into @p str.
530 *
531 * @param str Destination string.
532 * @return @c true if any data was available.
533 */
534 bool read_all_error(std::string& str);
535
536 /**
537 * @brief Reads all available stdout and stderr data into @p str.
538 *
539 * @param str Destination string.
540 * @return @c true if any data was available.
541 */
542 bool read_all(std::string& str);
543
544 /**
545 * @brief Writes @p buffer to the child's stdin.
546 *
547 * @param buffer Data to write.
548 * @param timeout_ms Maximum time to wait for the write to complete. Default: @c kDefaultWriteTimeoutMs.
549 * @return Number of bytes actually written.
550 */
551 size_t write(const std::vector<uint8_t>& buffer, int timeout_ms = kDefaultWriteTimeoutMs);
552
553 /**
554 * @brief Writes a string to the child's stdin.
555 *
556 * @param str String to write.
557 * @param timeout_ms Maximum time to wait. Default: @c kDefaultWriteTimeoutMs.
558 * @return Number of bytes actually written.
559 */
560 size_t write(const std::string& str, int timeout_ms = kDefaultWriteTimeoutMs);
561
562 /**
563 * @brief Closes the write channel (stdin pipe), signalling EOF to the child.
564 */
566
567 /**
568 * @brief Synchronously executes a program and waits for it to finish.
569 *
570 * @details
571 * Blocks until the program exits or @p timeout_ms elapses. Returns the exit code.
572 * Stdout and stderr are discarded.
573 *
574 * @param program Path to the executable.
575 * @param arguments Command-line arguments. Default: empty.
576 * @param timeout_ms Maximum wait time. Default: @c kDefaultExecuteTimeoutMs (30 s).
577 * @return Exit code, or -1 on timeout or start failure.
578 */
579 static int execute(const std::string& program, const std::vector<std::string>& arguments = {},
580 int timeout_ms = kDefaultExecuteTimeoutMs);
581
582 /**
583 * @brief Starts a program in the background and returns immediately.
584 *
585 * @details
586 * The started process is completely detached from the calling process.
587 * No handle is returned; the process runs until it exits on its own.
588 *
589 * @param program Path to the executable.
590 * @param arguments Command-line arguments. Default: empty.
591 * @return @c true if the process was successfully started.
592 */
593 static bool start_detached(const std::string& program, const std::vector<std::string>& arguments = {});
594
595 private:
596 struct ReadResult final {
597 bool has_stdout_data{false};
598 bool has_stderr_data{false};
599 bool truncated{false};
600 bool read_error{false};
601 };
602
603 void set_state(State state);
604
605 void set_error(Error error);
606
607 void cleanup();
608
609 ReadResult read_from_pipes();
610
611 void report_read_result(const ReadResult& result);
612
613 void read_from_pipes_with_lock();
614
615 bool setup_pipes();
616
617 bool start_program(const std::string& program, const std::vector<std::string>& arguments);
618
619 void monitor_thread();
620
621 void start_monitor_thread();
622
623 void stop_monitor_thread();
624
625 void handle_process_exit(int exit_code, ExitStatus status);
626
627 void invoke_callbacks_outside_lock(Error error_to_report, bool has_finished, int exit_code_to_report,
628 ExitStatus exit_status_to_report, State state_to_report, bool has_state_changed,
629 bool has_stdout_data, bool has_stderr_data);
630
631 std::unique_ptr<struct ProcessImpl> impl_;
632
634};
635
636} // namespace vlink
Platform-independent macro definitions for the VLink library.
#define VLINK_EXPORT
Definition macros.h:85
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition macros.h:184