# Process design

`Linux::Event::Process` combines pidfd lifecycle notification and optional
asynchronous standard I/O in one logical Loop object. It avoids PID-reuse races
and never runs Perl code in a post-fork child.

## Public type model

A concrete subclass defines `on_exit`:

```perl
package BuildProcess;
use parent 'Linux::Event::Process';

sub on_stdout ($process, $bytes) {
    print "build: $bytes";
}

sub on_stderr ($process, $bytes) {
    warn "build: $bytes";
}

sub on_exit ($process) {
    if (defined(my $code = $process->exit_code)) {
        say "build exited with $code";
    }
    else {
        say "build received signal " . $process->term_signal;
    }
    $process->loop->stop;
}
```

Callbacks are cached once per subclass. One Process object owns the pidfd,
stdio pipes, registrations, queue state, decoded status, and application data.

## Native spawning

`spawn` accepts an argument vector and never inserts a shell:

```perl
my $process = $loop->add(BuildProcess->spawn(
    command => ['/usr/bin/make', '-j4'], # required
    cwd     => '/srv/project',           # optional
    env     => { BUILD_MODE => 'test' }, # optional replacement environment
    stdin   => 'pipe',                   # optional; default inherit
    stdout  => 'pipe',                   # optional; default inherit
    stderr  => 'pipe',                   # optional; default inherit
    data    => { started_by => 'api' },  # optional
));
```

Construction stores a specification and has no process side effect. The child
is created when the object attaches through `loop => $loop` or `Loop->add`.
`pid` is therefore undefined while detached.

The XS extension uses `posix_spawnp`. Pipes are created atomically with
`O_CLOEXEC`. File actions establish stdio, close temporary pipe ends and the
original source descriptor after duplicating a caller handle, and optionally
change directory through
`posix_spawn_file_actions_addchdir_np`. No Perl interpreter, allocator,
callback, or application lock runs between fork and exec. This remains safe
after the resolver has started native threads.

`env` is a complete replacement environment. Omit it to inherit the current
environment. Use an explicit `['/bin/sh', '-c', 'make clean && make']` command
only when shell parsing is intentional.

## Standard I/O modes

Each standard descriptor accepts:

| Value | Child behavior | Parent ownership |
| --- | --- | --- |
| `inherit` | retains the corresponding parent descriptor | no new handle |
| `pipe` | connects to a nonblocking parent pipe end | Process owns parent end |
| `null` | connects to `/dev/null` | temporary handle closed after spawn |
| filehandle | duplicates it to stdio and closes the original child-side descriptor | caller keeps handle |
| `stdout` for stderr | duplicates child stdout to child stderr | follows stdout mode |

`on_stdout` and `on_stdout_eof` require `stdout => 'pipe'`. The corresponding
stderr callbacks require a stderr pipe, and `on_stdin_drain` requires a stdin
pipe. Impossible combinations fail during construction.

## Standard input and backpressure

`write_stdin` writes immediately when possible and queues the remainder. It
can be called while detached; queued bytes are retained until attachment.
Crossing `stdin_high_watermark` returns false while accepting the bytes.
`on_stdin_drain` fires when pending bytes reach `stdin_low_watermark`.

A nonzero `max_pending_stdin` is a separate hard safety limit. Overflow rejects
the unsent bytes, closes Process stdin, and reports `output_limit`. A prefix
already written directly before a partial write exposed the oversized
remainder cannot be recalled.

`close_stdin` is graceful: it rejects new writes, drains accepted bytes, then
closes the pipe to deliver EOF. Pipe writes block SIGPIPE only in the calling
thread, consume a signal generated by `EPIPE`, and restore the previous mask.
Process never installs a process-wide SIGPIPE disposition.

## Existing processes

An existing PID can be observed:

```perl
my $child = $loop->add(BuildProcess->new(
    pid  => $pid, # required
    reap => 1,    # default
));
```

`reap => 1` uses `waitid(P_PIDFD)` and requires the PID to be this process's
child. `reap => 0` supports lifecycle notification for a non-child but leaves
all status fields undefined. Opening the pidfd during attachment pins the
kernel identity even if the numeric PID is later reused.

## Exit and status

pidfd readability triggers nonblocking `waitid`. On a reaped child, Process
records one of an exit code or terminating signal, the core-dump flag, and a
conventional raw wait status. It then drains remaining stdout and stderr to
the current nonblocking boundary, closes stdin, and invokes `on_exit` exactly
once.

The final drain consumes all bytes already available when the owned child
exits, then closes the Process pipe ends. It deliberately does not keep a
Process alive for a descendant that inherited those descriptors.

The Loop remains available during `on_exit` and is released when the callback
returns, including when the callback throws. Callback exceptions propagate
from Loop dispatch after native cleanup.

## Signals and cancellation

`signal($number)` uses `pidfd_send_signal`, so it cannot target a new process
that reused the same numeric PID. Failures throw a structured `process` Error.

There is deliberately no `cancel`. Stopping notification, killing a process,
closing stdin, and waiting for exit are different operations. Applications
choose a signal and continue running the Loop until `on_exit` confirms the
result.

## Failure containment

If spawn succeeds but descriptor registration fails, Process sends SIGKILL
through the pidfd, reaps that exact child, closes every partial registration,
and makes the object failed before propagating the setup exception. It never
falls back from an available pidfd to a numeric-PID signal. A pidfd failure
immediately after spawn follows the same kill-and-reap rule before the PID can
escape the native spawn operation.

Asynchronous stdio errors use `process_io`; pidfd and wait failures use
`process`. Without `on_error`, Process warns and retains `last_error`.

## Ownership and Loop destruction

The Loop retains a running Process. Dropping an application reference does not
cancel notification or kill the child. Conversely, destroying the Loop closes
Linux::Event handles but does not secretly signal a process. Applications must
keep their Loop alive and define shutdown policy until owned children are
reaped, otherwise ordinary Unix zombie rules apply.

Spawned Processes and observed children with `reap => 1` exclusively own their
wait status. Do not combine them with `wait`, `waitpid`, or a separate
`SIGCHLD` reaper for the same PID. Use `reap => 0` when another component owns
reaping and only pidfd lifecycle notification is wanted.

## Platform contract

Process supports Linux 5.4 or newer for pidfd status and requires build headers
with `pidfd_open` and `pidfd_send_signal` syscall definitions. The build also
requires a libc with `posix_spawn_file_actions_addchdir_np`. These constraints
are explicit because a fallback based on `fork` plus Perl child setup would be
unsafe once native worker threads exist.
