NAME

Linux::Event::Kernel::Process - pidfd process lifecycle and asynchronous stdio

SYNOPSIS

use v5.36;
use Linux::Event::Loop;
use Linux::Event::Kernel::Process;

my $loop = Linux::Event::Loop->new;
my $worker = Linux::Event::Kernel::Process->spawn(
    loop    => $loop,
    command => [$^X, '-e', 'print "hello\\n"'],
    stdout  => 'pipe',
    on_stdout => sub ($process, $bytes) {
        print $bytes;
    },
    on_exit => sub ($process) {
        say 'exit code: ' . $process->exit_code
            if defined $process->exit_code;
        $process->loop->stop;
    },
);
$loop->run;

DESCRIPTION

Linux::Event::Kernel::Process is the public process leaf. One object combines process creation or observation, pidfd identity-safe lifecycle notification, optional asynchronous stdin/stdout/stderr, decoded exit status, and pidfd-based signaling.

Linux::Event uses posix_spawnp for spawned children and never runs Perl code in a post-fork child. pidfd operations avoid directing signals or lifecycle state at an unrelated process after numeric PID reuse.

CALLBACKS, SUBCLASSING, AND TUNING

new accepts on_exit and on_error as constructor coderefs. spawn also accepts the stdout, stderr, EOF, and stdin-drain callbacks listed below. Closures are convenient for per-process lexical state; subclasses provide reusable named behavior and a single place for Process I/O tuning:

package BuildProcess;
use parent 'Linux::Event::Kernel::Process';

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

process_options also configures stdin high/low watermarks and the maximum pending stdin bound. Linux::Event validates and caches this policy and the class callbacks once per subclass. Constructor callbacks override same-named methods for one object and are retained once in its effective descriptor; no event-time method lookup or callback-style branch is added.

process_options

Define process_options as a class method on the Process subclass. It returns key/value pairs, or one hash reference:

package BuildProcess;
use parent 'Linux::Event::Kernel::Process';

sub process_options ($class) {
    return (
        read_size            => 131_072,
        max_reads_per_tick   => 32,
        max_pending_stdin    => 8_388_608,
    );
}

spawn accepts the same names as per-process overrides. The complete option set is:

  • read_size (default 65,536)

    Positive maximum byte size of one stdout or stderr callback payload.

  • max_reads_per_tick (default 64)

    Positive maximum successful reads from each child output pipe during one readiness dispatch, providing fairness between active resources.

  • stdin_high_watermark (default 1,048,576)

    Non-negative pending-stdin byte level at which write_stdin begins returning false while still accepting the bytes.

  • stdin_low_watermark (default 262,144)

    Non-negative pending-stdin byte level at or below which on_stdin_drain fires after high-watermark backpressure. It must not exceed stdin_high_watermark.

  • max_pending_stdin (default 0)

    Hard non-negative pending-stdin byte limit. Zero means unbounded.

Linux::Event validates and caches these integer values once per concrete subclass.

SPAWNING

spawn accepts a command argument vector and does not insert a shell:

my $process = Linux::Event::Kernel::Process->spawn(
    loop    => $loop,                       # optional
    command => ['/usr/bin/make', '-j4'],    # required
    cwd     => '/srv/project',              # optional
    env     => { BUILD_MODE => 'test' },    # optional replacement env
    stdin   => 'pipe',                      # optional
    stdout  => 'pipe',                      # optional
    stderr  => 'pipe',                      # optional
    data    => $state,                      # optional
    on_stdout => sub ($process, $bytes) { print $bytes },
    on_exit   => sub ($process) { $process->loop->stop },
);

Construction is side-effect free while detached. The child is created when the object attaches through loop => $loop or $loop->add($process). Consequently pid is undefined before attachment.

env replaces the complete environment when supplied; omit it to inherit the current environment. Use an explicit shell in command only when shell syntax is intentionally required.

STANDARD I/O

Each stdio option accepts inherit, pipe, null, or a caller filehandle. stderr may additionally be stdout to merge child stderr into child stdout.

Pipe callbacks are:

sub on_stdout ($process, $bytes) { ... }
sub on_stdout_eof ($process) { ... }
sub on_stderr ($process, $bytes) { ... }
sub on_stderr_eof ($process) { ... }
sub on_stdin_drain ($process) { ... }

Readable child pipes are drained by the native process I/O helper while preserving read_size callback chunking and max_reads_per_tick fairness.

write_stdin($bytes) writes immediately when possible and queues the remainder. High/low watermarks provide cooperative flow control and max_pending_stdin can impose a hard safety bound. close_stdin drains already accepted input, then closes the child's input pipe to deliver EOF.

OBSERVING AN EXISTING PROCESS

An existing PID may be observed instead of spawned:

my $process = Linux::Event::Kernel::Process->new(
    pid  => $pid,
    reap => 1,
    on_exit => sub ($process) { ... },
);
$loop->add($process);

reap => 1 is the default and requires a child process whose status this object owns. reap => 0 permits lifecycle notification for a non-child but leaves decoded wait-status fields undefined.

EXIT CALLBACK AND STATUS

A subclass defines on_exit($process), or construction supplies on_exit => sub ($process) { ... }. When a reaped child exits, Linux::Event records either exit_code or term_signal, plus the core-dump flag and conventional raw wait status. Remaining available stdout/stderr bytes are drained before on_exit.

The Loop remains available during on_exit and is released after callback completion. Callback exceptions propagate after native cleanup.

SIGNALS

signal($number) uses pidfd_send_signal rather than a bare numeric PID and returns the Process object. Failures are structured Linux::Event::Error values.

There is deliberately no generic cancel. Stopping observation, closing stdin, asking a child to terminate, and confirming process exit are distinct operations. Applications choose an explicit signal and continue running the Loop until on_exit confirms lifecycle completion.

ERRORS AND OWNERSHIP

Optional on_error($process, $error) receives asynchronous process or stdio failures. Without it Linux::Event warns and retains last_error.

The Loop retains a running Process. Destroying the Loop closes Linux::Event resources but does not secretly kill the child. Spawned processes and observed children with reap => 1 exclusively own their wait status; do not also use a competing waitpid or SIGCHLD reaper for the same child.

PLATFORM

Process requires Linux pidfd support and build headers for pidfd_open and pidfd_send_signal. The runtime lifecycle/status path targets Linux 5.4 or newer. The build also requires libc support for posix_spawn_file_actions_addchdir_np.

SEE ALSO

Linux::Event::Loop, docs/PROCESS-DESIGN.md.