Captured process helpers
The shared invocation model supports terminal children, captured helpers and independently living processes. These are separate ownership contracts: a terminal is not a private byte pipe, and releasing a helper's descendants is not a claim that those descendants have escaped the caller's ancestor jobs.
One invocation model
core::ProcessLaunchSpec describes a program's invocation independently of
whether its standard streams are connected to a terminal or to private byte
pipes. TerminalLaunchSpec inherits these fields and adds its terminal profile
and required terminal exit policy. Both platform terminal backends already use
the shared native preparation code.
program(executable, arguments) names the executable without a PATH search;
arguments do not include argv[0]. An empty argv0 uses the executable name.
working_directory defaults to / and must be explicitly set to a usable
native directory. ProcessEnvironmentPolicy::InheritAndOverride starts from
the parent's environment and replaces named entries; ExplicitOnly starts
empty. Environment names must be nonempty, contain no =, and not repeat.
Windows additionally compares names case-insensitively.
windows_command_processor(executable, command) explicitly selects cmd syntax.
An absent command opens interactive cmd with AutoRun disabled; a present command,
including empty text, executes and exits. Command text is trusted code, not data
to be escaped. It cannot be combined with an argument vector or argv[0]; POSIX
refuses this form. Windows uses /d or /d /s /c and native image separators,
while ordinary argument data uses the existing CRT encoder. No shell is guessed.
The common pure validator rejects NULs in every native string, invalid names and contradictory forms before spawning. POSIX permits opaque byte paths/arguments; Windows requires valid UTF-8 and enforces native command-line limits. The native backends share one environment/argv builder per platform, not a helper-specific copy of the terminal's rules.
Captured execution contract
core::ProcessRunner is injected and has no default/global instance. Its
run(request) synchronously borrows the request's opaque binary input only
for that call. Input is stdin, never part of argv, and is closed after the last
byte, including empty input. Each output stream has an independent capture
limit; zero means retain nothing while draining. Excess data sets truncated.
These bytes are private diagnostic captures, not terminal output.
The idle-progress budget must be positive. Only positive byte transfer resets
it; repeated wakeups, interruptions and retry answers do not. Native adapters
borrow a monotonic Clock, keep each pump bounded and introduce no persistent
I/O worker thread. Completion follows root exit and already-ready diagnostics,
not EOF held open by a background descendant.
The caller must choose ReleaseOnSuccess or TerminateOnCompletion; an
unspecified descendant policy is an invalid request. Successful release permits
a clipboard holder to survive its root. Failure/timeout always cleans the owned
group/job. POSIX containment is a process group, not a claim about daemons that
deliberately leave it; Windows containment is an assigned job, with the root
suspended until assignment and only private stdio handles whitelisted.
ProcessRunResult distinguishes invalid request, launch failure, I/O failure,
idle timeout, unavailable exit status and completed root exit. The native error
retains its domain/code; the exit status retains either a normal exit code
(including every Windows DWORD) or a POSIX signal. Absence is not zero.
successful() requires a completed result, all input transferred, no native
error, and a known normal zero exit. Stdout/stderr captures and their truncation
remain independent of that verdict.
Native construction
Executable preflight on Windows
Added in ckVision0.1.18. This native inspection is separate from shell selection and from the process launch, captured-helper and detached-lifetime APIs.
term::inspect_windows_process_image(absolute_utf8_path) reads bounded PE/COFF
headers without executing the file or searching PATH. It distinguishes invalid
paths, unavailable observations, malformed/non-process images, unsupported
architectures and header-compatible candidates. DLLs and non-Windows subsystems
are refused. The declared machine is checked against host user-mode support,
not the architecture of the inspecting application. Windows11 uses
GetMachineTypeAttributes; the Windows10 baseline checks the native machine
and documented x86 compatibility using IsWow64Process2.
This matters for an x64 application on ARM64: an observed system cmd.exe
returns error193 from GetBinaryTypeW in the x64 caller, yet launching the
same image succeeds. It is therefore not a reliable cross-architecture
preflight. No shell names, server roles or application policy enter inspection.
WindowsProcessImageInfo::compatible() is not loader success, publisher
authentication or readiness. It does not resolve imports, validate all image
contents or prevent the file from changing after inspection. The eventual
launch operation remains authoritative and must handle failure independently.
Authority: Microsoft's PE/COFF format, machine support query and Windows on Arm emulation.
Independently living Windows processes
core::DetachedProcessLauncher is a separate injected boundary, implemented on
Windows by term::WindowsDetachedProcessLauncher. It uses ProcessLaunchSpec
and the same native preparation, inherits no handles, and detaches the console.
It requests job breakaway but creates the child suspended. Before resuming,
IsProcessInJob(child, nullptr) must report no remaining job: an inner job's
permission alone is insufficient. Restrictive ancestry is an explicit failure,
not a fallback that reports a daemon started and later loses it with its caller.
DetachedProcessResult preserves the failure stage, native error, full-width
identity and separate cleanup state/error. Rejected created children have not
run application code; cleanup terminates and waits at most five seconds.
successful() requires a verified resumed child. Success releases process
ownership, but does not imply application readiness. No POSIX detached backend
is claimed by this Windows-specific implementation. Captured-helper descendant
release does not claim independence from the caller's ancestor jobs.
Captured helpers
Include cvision/term/process_runner.hpp, construct a NativeProcessRunner
with the application's existing monotonic Clock, and inject it through the
core interface. The clock must outlive the runner and advance during real
native waits. This is a synchronous host-boundary call, not an asynchronous
UI task: the application chooses where a bounded helper call belongs.
POSIX owns nonblocking pipes and a dedicated process group. Only stdin, stdout and stderr survive exec. macOS uses its atomic close-by-default spawn file actions rather than one close syscall per possible descriptor; cwd/exec errors are returned by the native spawn call. Other POSIX hosts use a private close-on-exec error channel instead of reducing cwd/exec errors to exit127. The macOS directory action supports macOS10.15 onward and selects the standard replacement on macOS26; its deprecated spelling is only the older-host fallback. The calling thread's SIGPIPE mask is temporarily protected without replacing the process-global signal disposition, then restored. Windows owns three overlapped parent pipe endpoints, current-user-only ACLs, unique per-call names and a suspended root assigned to a kill-on-close job before it runs. Only its three child stdio ends are on the inherited-handle whitelist. Successful release clears kill-on-close; every other path retains owned-job cleanup. Win32 and NTSTATUS errors have distinct domains; neither is a POSIX errno.
Portable tests use an injected fake to check this value contract. Native terminal tests retain real argument, cwd/environment and child-lifecycle coverage; real captured-pipe fixtures cover binary transfer, independent capped floods, timeouts/progress, launch/exit failures, inheritance and descendant policies. POSIX tests also force real pipe-resource failure, verify timeout-root reaping, and deliver repeated signal wakeups without extending the idle budget. The external child fixture is an ordinary strict-warning executable; sanitizer instrumentation remains on the library and test parent, not an external runtime's startup/exit latency. Final platform gates and downstream integration remain required before release.