Skip to main content

subsystem-summary-of-process

read this skill for a token-efficient summary of the process subsystem

Source facts

Repository
stellar/stellar-core
Last source activity
February 11, 2026 at 01:44
Detected SKILL.md language
English
Stars
3,302
Forks
1,079

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
subsystem-summary-of-process
description
read this skill for a token-efficient summary of the process subsystem
# Process Subsystem — Technical Summary ## Overview The process subsystem provides asynchronous subprocess management for stellar-core, wrapping platform-specific process spawning (POSIX `posix_spawnp` and Windows `CreateProcess`) behind a unified asio-integrated interface. It enables running external commands (e.g., history archival tools like `gzip`, `gunzip`, `curl`) asynchronously, with configurable concurrency limits, output file capture, graceful shutdown, and process lifecycle tracking. No facilities exist for reading/writing subprocess I/O ports — this is strictly for "run a command, wait to see if it worked." ## Key Files - **ProcessManager.h** — Abstract interface `ProcessManager` and the `ProcessExitEvent` class for async process completion notification. - **ProcessManagerImpl.h / ProcessManagerImpl.cpp** — Concrete implementation of `ProcessManager`; contains all lifecycle management, platform-specific spawning, signal handling, and shutdown logic. - **PosixSpawnFileActions.h / PosixSpawnFileActions.cpp** — POSIX-only RAII wrapper around `posix_spawn_file_actions_t` for redirecting subprocess stdout to a file. --- ## Key Classes and Data Structures ### `ProcessManager` (abstract, inherits `std::enable_shared_from_this<ProcessManager>`, `NonMovableOrCopyable`) The public interface for subprocess management. One `ProcessManager` exists per `Application` instance, created via the static factory `ProcessManager::create(Application&)`. **Key virtual methods:** - `runProcess(cmdLine, outputFile)` → `std::weak_ptr<ProcessExitEvent>` — Queues or immediately launches a subprocess. If `outputFile` is non-empty, stdout is captured to a temp file and atomically renamed on success. - `getNumRunningProcesses()` — Count of active (non-shutting-down) child processes. - `getNumRunningOrShuttingDownProcesses()` — Count of all tracked child processes including those being terminated. - `tryProcessShutdown(pe)` — Synchronously cancels a `ProcessExitEvent` and attempts to terminate the associated process (SIGTERM on POSIX, `GenerateConsoleCtrlEvent` on Windows). Returns `true` if the termination signal was sent successfully. - `shutdown()` — Marks the manager as shut down, cancels all pending processes, and attempts polite shutdown of all running processes. - `isShutdown()` — Returns whether shutdown has been initiated. ### `ProcessExitEvent` An asio-compatible event object that clients use to await subprocess completion. It simulates an event notifier using a `RealTimer` set to maximum duration. When the subprocess exits (or is cancelled), the timer is cancelled with an appropriate error code. **Members:** - `mTimer` (`std::shared_ptr<RealTimer>`) — The underlying asio timer used for async waiting. - `mImpl` (`std::shared_ptr<Impl>`) — Platform-specific implementation details (command line, output file, process handle, lifecycle state). - `mEc` (`std::shared_ptr<asio::error_code>`) — Shared error code for communicating the real exit status past asio's timer cancellation (which always delivers `operation_aborted`). **Key methods:** - `async_wait(handler)` — Registers a callback that fires when the process exits. The handler receives an `asio::error_code` where a zero value means success, and a non-zero value encodes the process exit status. ### `ProcessExitEvent::Impl` (internal, inherits `std::enable_shared_from_this`) Holds all per-process state and manages the platform-specific spawning logic. **Members:** - `mOuterTimer` / `mOuterEc` — Shared pointers back to the owning `ProcessExitEvent`'s timer and error code. - `mCmdLine` (`std::string const`) — The command line to execute. - `mOutFile` (`std::string const`) — The desired final output file path (may be empty if no capture). - `mTempFile` (`std::string const`) — Temporary file path for capturing stdout before atomic rename. - `mLifecycle` (`ProcessLifecycle`) — Tracks the process through its states. - `mProcessId` (`int`) — PID of the spawned process (-1 before launch). - `mProcessHandle` (Windows only, `asio::windows::object_handle`) — Waitable handle for the Windows process object. - `mProcManagerImpl` (`std::weak_ptr<ProcessManagerImpl>`) — Weak back-reference to the owning manager. **Key methods:** - `run()` — Platform-specific process spawning. On POSIX: splits the command line, sets up `PosixSpawnFileActions` for output redirection, sets `FD_CLOEXEC` on all open file descriptors ≥ 3, then calls `posix_spawnp()`. On Windows: sets up `STARTUPINFOEX` with inheritable handles, calls `CreateProcess()`, and registers an async wait on the process handle. - `finish()` — Called on termination; renames the temp file to the final output file (atomic move). Returns `false` if the rename fails. - `cancel(ec)` — Sets the outer error code and cancels the outer timer, firing all registered `async_wait` handlers. ### `ProcessManagerImpl` (inherits `ProcessManager`) The concrete implementation that manages the full lifecycle of subprocesses. **Members:** - `mProcessesMutex` (`std::recursive_mutex`) — Guards `mProcesses` and `mPending` since subprocess exits arrive asynchronously. - `mProcesses` (`std::map<int, std::shared_ptr<ProcessExitEvent>>`) — Maps PID to running or shutting-down processes. - `mPending` (`std::deque<std::shared_ptr<ProcessExitEvent>>`) — Queue of processes waiting to be launched (when concurrency limit is reached). - `mIsShutdown` (`bool`) — Set once `shutdown()` is called. - `mMaxProcesses` (`size_t const`) — Maximum concurrent subprocesses, from `Config::MAX_CONCURRENT_SUBPROCESSES`. - `mIOContext` (`asio::io_context&`) — The application's I/O context for async operations. - `mSigChild` (`asio::signal_set`) — On POSIX, listens for `SIGCHLD` to detect child process exits. Unused on Windows. - `mTmpDir` (`std::unique_ptr<TmpDir>`) — Temporary directory for capturing subprocess output files. - `mTempFileCount` (`uint64_t`) — Monotonic counter for generating unique temp file names. ### `ProcessLifecycle` (enum, anonymous namespace) Tracks the state of a subprocess through its lifetime: - `PENDING` (0) — Queued, not yet spawned. - `RUNNING` (1) — Spawned, waiting for exit. - `TRIED_POLITE_SHUTDOWN` (2) — SIGTERM (POSIX) or CTRL_C_EVENT (Windows) sent. - `TRIED_FORCED_SHUTDOWN` (3) — SIGKILL (POSIX) or TerminateProcess (Windows) sent. - `TERMINATED` (5) — Exit detected and handled. ### `PosixSpawnFileActions` (POSIX only) RAII wrapper around `posix_spawn_file_actions_t`. Lazily initializes the actions object on first `addOpen()` call. Provides an implicit conversion to `posix_spawn_file_actions_t*` (returns `nullptr` if never initialized, meaning no file actions). **Key methods:** - `addOpen(fildes, fileName, oflag, mode)` — Registers a file-open action for the child process (used to redirect fd 1 / stdout to a temp file). - `initialize()` — Calls `posix_spawn_file_actions_init()`; idempotent. - Destructor calls `posix_spawn_file_actions_destroy()` if initialized. --- ## Key Control Flows ### Process Launch Flow 1. Client calls `ProcessManagerImpl::runProcess(cmdLine, outFile)`. 2. A new `ProcessExitEvent` is created with a `RealTimer` set to max duration. 3. A `ProcessExitEvent::Impl` is created holding the command line, output file, a generated temp file path, and a weak reference to the manager. 4. The event is pushed onto `mPending`. 5. `maybeRunPendingProcesses()` is called, which pops events from `mPending` while `getNumRunningOrShuttingDownProcesses() < mMaxProcesses`. 6. For each dequeued event, `Impl::run()` is called: - **POSIX:** Command line is split on whitespace into argv. `PosixSpawnFileActions` is set up if output capture is needed. All file descriptors ≥ 3 are marked `FD_CLOEXEC`. `posix_spawnp()` is called. Lifecycle transitions to `RUNNING`. - **Windows:** `STARTUPINFOEX` and handle inheritance are configured. `CreateProcess()` is called with `CREATE_NEW_PROCESS_GROUP`. An async wait is registered on the process handle. Lifecycle transitions to `RUNNING`. 7. The PID is recorded in `mProcesses`. 8. A `weak_ptr<ProcessExitEvent>` is returned to the caller, who calls `async_wait()` to register a completion handler. ### Process Exit Handling (POSIX) 1. `SIGCHLD` arrives, handled by `asio::signal_set` → `handleSignalChild()`. 2. `handleSignalChild()` re-registers the signal handler (via `startWaitingForSignalChild()`), then calls `reapChildren()`. 3. `reapChildren()` iterates all tracked PIDs, calling `waitpid(pid, &status, WNOHANG)` for each. 4. For each successfully reaped child, `handleProcessTermination(pid, status)` is called. 5. `handleProcessTermination()` maps the exit status to an `asio::error_code` (via `mapExitStatusToErrorCode`), calls `Impl::finish()` to rename the temp output file, removes the process from `mProcesses`, calls `maybeRunPendingProcesses()` to launch queued processes, then fires the callback via `Impl::cancel(ec)`. ### Process Exit Handling (Windows) 1. The `asio::windows::object_handle::async_wait` fires when the process handle becomes signaled. 2. The callback calls `GetExitCodeProcess()` and passes the result to `handleProcessTermination()`. 3. The rest follows the same flow as POSIX. ### Shutdown Flow 1. `shutdown()` sets `mIsShutdown = true`. 2. All pending (not yet launched) processes are cancelled with `ABORT_ERROR_CODE`. 3. `tryProcessShutdownAll()` iterates all running processes and calls `tryProcessShutdown()` on each. 4. `tryProcessShutdown()` uses a two-phase approach based on lifecycle state: - If `RUNNING`: calls `politeShutdown()` (SIGTERM / CTRL_C_EVENT), advances to `TRIED_POLITE_SHUTDOWN`. - If `TRIED_POLITE_SHUTDOWN`: calls `forcedShutdown()` (SIGKILL / TerminateProcess), advances to `TRIED_FORCED_SHUTDOWN`. 5. The destructor (`~ProcessManagerImpl()`) ensures cleanup: it cancels the `SIGCHLD` handler, calls `shutdown()`, then loops up to 3 times sleeping 10ms, reaping children, and re-triggering progressively more forceful shutdown. ### Concurrency Control - The maximum number of concurrent subprocesses is controlled by `mMaxProcesses` (from `Config::MAX_CONCURRENT_SUBPROCESSES`). - When the limit is reached, new processes are queued in `mPending` (a FIFO deque). - After each process exit (`handleProcessTermination`), `maybeRunPendingProcesses()` is called to launch queued processes up to the limit. - All access to `mProcesses` and `mPending` is guarded by `mProcessesMutex` (a recursive mutex), since process exits arrive asynchronously from signal handlers or async I/O callbacks. --- ## Ownership Relationships ``` Application └── ProcessManagerImpl (shared_ptr, via ProcessManager::create) ├── mProcesses: map<pid, shared_ptr<ProcessExitEvent>> │ └── ProcessExitEvent │ ├── mTimer: shared_ptr<RealTimer> │ ├── mEc: shared_ptr<asio::error_code> │ └── mImpl: shared_ptr<ProcessExitEvent::Impl> │ └── mProcManagerImpl: weak_ptr<ProcessManagerImpl> (back-ref) ├── mPending: deque<shared_ptr<ProcessExitEvent>> ├── mSigChild: asio::signal_set (POSIX only) └── mTmpDir: unique_ptr<TmpDir> ``` - `ProcessManagerImpl` owns all `ProcessExitEvent` objects (via `mProcesses` and `mPending`). - `ProcessExitEvent::Impl` holds a `weak_ptr` back to `ProcessManagerImpl` to avoid circular ownership. - Callers receive a `weak_ptr<ProcessExitEvent>` from `runProcess()`, so the manager controls the event's lifetime. - The `Impl::run()` method on Windows captures a `shared_from_this()` to keep `Impl` alive through the async wait callback. --- ## Key Data Flows 1. **Command → Process:** `runProcess(cmdLine, outFile)` → queued in `mPending` → dequeued by `maybeRunPendingProcesses()` → `Impl::run()` spawns the OS process. 2. **Process Exit → Callback:** OS signal (SIGCHLD) or handle wait → `reapChildren()` / handle callback → `handleProcessTermination()` → `Impl::finish()` (rename temp file) → `Impl::cancel(ec)` → timer cancelled → `async_wait` handler fires with exit code. 3. **Output Capture:** Subprocess stdout is redirected to a temp file in `mTmpDir`. On successful termination, `Impl::finish()` atomically renames the temp file to the requested output file path. If the output file already exists, the rename fails and an error is propagated. 4. **Exit Code Mapping:** `mapExitStatusToErrorCode()` translates OS-level exit status (including POSIX `WIFEXITED`/`WEXITSTATUS` macros) into `asio::error_code`. Special handling for exit code 127 on Linux (likely missing command). On Windows, the exit code is used directly. 5. **Shutdown Signal Flow:** `shutdown()` → cancel pending → polite shutdown (SIGTERM) → forced shutdown (SIGKILL) → destructor reaps remaining children with retry loop. --- ## Platform Differences | Aspect | POSIX | Windows | |--------|-------|---------| | Process spawning | `posix_spawnp()` | `CreateProcess()` with `EXTENDED_STARTUPINFO_PRESENT` | | Exit detection | `SIGCHLD` via `asio::signal_set` + `waitpid(WNOHANG)` | `asio::windows::object_handle::async_wait` | | Output redirection | `posix_spawn_file_actions_addopen()` on fd 1 | `CreateFile()` + `STARTF_USESTDHANDLES` | | Polite shutdown | `kill(pid, SIGTERM)` | `GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)` | | Forced shutdown | `kill(pid, SIGKILL)` | `TerminateProcess()` | | FD cleanup | `FD_CLOEXEC` on fds 3..SC_OPEN_MAX (with gap heuristic) | Handle inheritance via `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` | --- ## Error Handling Notes - If `posix_spawnp()` or `CreateProcess()` fails, the `ProcessExitEvent` is cancelled with `std::errc::io_error` and the error is logged. - On Linux, exit code 127 triggers a prominent warning about a likely missing command (since `posix_spawnp` does not fault on file-not-found in the parent). - The timer-based `async_wait` pattern works around asio always delivering `operation_aborted` on timer cancel: the real error code is stored in a shared `mEc` variable and the handler reads from that instead of using the asio-provided code. - `checkInvariants()` validates consistency: pending processes must be in `PENDING` state, running processes must not be `PENDING`, and PIDs must match map keys. Called at key state transitions.
View on GitHub