Package · thread

API Reference

Complete reference for every public type in Flytachi\Winter\Thread. Signatures match the source exactly; each entry lists parameters, return values, exceptions, and the non-obvious semantics.

API at a glance

The surface splits into two tiers. Most applications only touch the Public API — three types. The low-level API is for building your own pool, scheduler, or supervisor directly on the raw primitives.

Public API — what you use

Type Kind You use it to…
Runnable interface define a task — put your logic in run()
Thread class start & control one task as a background process
Launcher interface how a process is spawned; bound once at bootstrap
AdaptiveLauncher · CliLauncher · SwooleLauncher class three backends — routing, CLI/FPM, coroutine

Low-level & extension API — what you build on

Type Kind Role
Launcher · CliLauncher interface / class spawn a process → return a handle
ProcessHandle class drive one process (reap/join/detach/signal/read)
LaunchSpec DTO all launch parameters in one value object
PayloadTransport · Pipe / TempFile / Shm interface / class payload delivery strategy
StagedPayload DTO the staging result a launcher consumes
Runner · AdaptiveRunner interface / class child-side execution
Signal class raw-PID POSIX helpers (zombie-aware)
ThreadException class the library’s only exception

The four interfaces (Launcher, ProcessHandle, Runner, PayloadTransport) are the extension points that accept your own implementation; everything else is a concrete type you consume.


Public API

Runnable (interface)

Flytachi\Winter\Thread\Runnable — the task contract.

php
public function run(array $args): void

Put your logic in run(); it executes in the worker process.

Parameter Type Description
$args array<string, string|bool> per-run values from start() — flags arrive as boolean true, all other values as strings

Returns nothing. Signal the outcome via the exit code (return/normal completion → 0; throwing → non-zero) or side effects (file/DB/queue).

Contract: the implementing object must be serializable — no live resources (PDO, sockets, streams) in its properties; open them inside run(). An uncaught exception is caught by the runner, logged to STDERR with a trace, and turned into a non-zero exit.


Thread (final class)

Flytachi\Winter\Thread\Thread — the high-level facade over one process. Mutable; guards against concurrent starts.

__construct

php
new Thread(
  Runnable $runnable,
  string   $namespace = '',
  ?string  $name = null,
  ?string  $tag = null,
)
Parameter Type Default Description
$runnable Runnable the task to execute
$namespace string '' logical grouping, shown in the OS process title
$name ?string null task name; when null, the Runnable’s short class name (or 'anonymous' for anonymous classes)
$tag ?string null instance discriminator; shown as @tag (defaults to @runnable in the title)

Constructing does not start anything. The process title is WinterThread <namespace> -> <name>@<tag> (only where cli_set_process_title() exists).

Static — launcher binding

Method Returns Description
Thread::bindLauncher(Launcher $launcher) void set the process-wide launcher (call once at bootstrap)
Thread::launcher() Launcher the current launcher; lazily creates an AdaptiveLauncher if none is bound

start

php
start(
  array   $arguments = [],
  bool    $debugMode = false,
  ?string $outputTarget = '/dev/null',
  bool    $detached = false,
): int

Serializes the task, launches the process, and returns immediately.

Parameter Type Default Description
$arguments array<string, scalar|null> [] per-run args exposed in run()’s $args. true → valueless flag; false/null → dropped; other scalars → strings; non-scalars ignored
$debugMode bool false enable E_ALL + display_errors in the child
$outputTarget ?string '/dev/null' '/dev/null' discards (no pipe); a path appends (mode a); null pipes to the parent for readOutput()/readError()
$detached bool false daemonize (fork+setsid) for zombie-free fire-and-forget

Returns int — the launched PID (in detached mode, the launcher’s ephemeral PID). Throws ThreadException if the thread is already alive, or the process fails to start.

Reading piped output

Passing null pipes stdout/stderr to the parent. join() and reap() drain those pipes while they wait, so a bare join() never deadlocks and readOutput() after it returns the full output. The Broken pipe stall only happens in true fire-and-forget — you pass null and then never read, join(), or reap(); for that case keep the '/dev/null' default. See Output & Broken Pipe.

Lifecycle & waiting

Method Returns Description
join(int $timeout = 0) ?int block (poll 50 ms) until exit, draining the stdout/stderr pipes while it waits; returns the exit code, null on timeout (seconds; 0 = forever), or -1 if never started or the worker was signal-killed. Reaps on completion
reap() bool non-blocking: drains the pipes, then true if finished/absent (and reaped, exit code set), false if still running
detach() void stop tracking (non-blocking; no proc_close). Afterwards isAlive()false, reap()true, signals → false, exit code never collected. Note: detach() ≠ detached mode

State

Method Returns Description
getPid() ?int launched PID, or null before start(); the launcher’s PID in detached mode
isAlive() bool is the process running now? false before start / after finish / after detach()
getExitCode() ?int exit code once reaped (join/reap); -1 if the worker was signal-killed; null before reaping, and null forever after detach()

Signals (require ext-posix; each returns false if not running)

Method Signal Description
pause() SIGSTOP suspend (unblockable)
resume() SIGCONT resume a paused worker
interrupt() SIGINT Ctrl+C equivalent (catchable)
terminate() SIGTERM graceful stop request (catchable)
kill() SIGKILL force kill (unblockable)

Each returns booltrue if the signal was sent. To react gracefully, the task must install a handler; see Graceful Shutdown.

Output (non-empty only when started with outputTarget: null)

Method Returns Description
readOutput() string STDOUT received since the last call (consuming, non-blocking); '' if none / not piped. Buffered by join()/reap(), so it still returns the full output after the process finishes
readError() string STDERR received since the last call (consuming, non-blocking); '' if none / not piped

Metadata

Method Returns Description
getNamespace() string the namespace
getName() string the resolved task name
getTag() ?string the tag, or null

Binding a launcher

Launch configuration lives in a Launcher — documented below, under the low-level API, because it is also the extension point. Bind it once at bootstrap:

php
Thread::bindLauncher(CliLauncher::adaptive(secret: ''));

Thread::launcher();   // the current launcher; an AdaptiveLauncher when none is bound

Low-level & extension API

For pools, schedulers, and custom backends. Thread is built on exactly these pieces — you can drive them directly.

Launcher (interface)

Flytachi\Winter\Thread\Launch\Launcher — how a process is spawned. It lives parent-side only: it is never shipped to the child, where a separate AdaptiveRunner runs, and the secret travels through the WINTER_THREAD_SECRET env var.

php
interface Launcher
{
  public function launch(LaunchSpec $spec): ProcessHandle;
  public function security(): ?DefaultSecurityProvider;
}
Method Returns Description
launch($spec) ProcessHandle spawn the process and return a control handle; throws ThreadException on failure
security() ?DefaultSecurityProvider the payload-signing provider, or null when no secret

Two methods is the entire surface: transport, binary path and runner path are not part of it — they are a concrete backend’s own business. So your own way of launching (SSH, a container, a remote node) implements exactly what it needs.

AdaptiveLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\AdaptiveLauncher — what is used when you bind nothing. It holds both built-in backends and picks one per launch() from the runtime at that moment: SwooleLauncher inside a Swoole coroutine or with runtime hooks enabled, CliLauncher everywhere else.

php
new AdaptiveLauncher(CliLauncher $cli, SwooleLauncher $swoole)

AdaptiveLauncher::adaptive(
  ?string $secret = null,
  ?string $binaryPath = null,
  ?string $runnerPath = null,
): AdaptiveLauncher

CliLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\CliLauncher — the CLI and FPM backend, proc_open-based. The command is built fully escapeshellarg-escaped.

php
new CliLauncher(
  string            $binaryPath,
  string            $runnerPath,
  ?PayloadTransport $transport = null,
  ?string           $secret = null,
)

CliLauncher::adaptive(
  ?string           $secret = null,
  ?PayloadTransport $transport = null,
  ?string           $binaryPath = null,
  ?string           $runnerPath = null,
): CliLauncher
Parameter Resolved when null
$secret the WINTER_THREAD_SECRET env var, else null (no signing)
$transport not resolved at construction — chosen on every launch(): TempFileTransport under an active Swoole runtime, PipeTransport otherwise
$binaryPath CLI SAPI → PHP_BINARY; non-CLI (FPM/CGI) → PHP_BINDIR/php if executable, else 'php'
$runnerPath the packaged wRunner

Immutable — to change one part, construct a new one.

SwooleLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\SwooleLauncher — the backend for an active Swoole runtime. Instead of proc_open (which corrupts the reactor’s descriptors) or Swoole\Process (forbidden while async-io threads are up), it starts the runner as a shell background job through Swoole\Coroutine\System::exec().

php
new SwooleLauncher(string $binaryPath, string $runnerPath, ?string $secret = null)

SwooleLauncher::adaptive(
  ?string $secret = null,
  ?string $binaryPath = null,
  ?string $runnerPath = null,
): SwooleLauncher

There is no transport in the signature: it picks delivery itself — shared memory with ext-shmop, a temp file otherwise. It requires ext-swoole at launch time and is meaningful only for detached tasks: a coroutine worker has no parent pipe, so its handle is PID-based — liveness and signalling work, but there is nothing to read or join().


ProcessHandle (interface)

Flytachi\Winter\Thread\Launch\ProcessHandle — the low-level process-control primitive a launcher returns. Not constructed directly — obtained from Launcher::launch().

It is an interface: your own backend returns its own implementation. Two ship with the package — CliProcessHandle (wraps the proc_open resource and tracks process, pipe and exit-code state) and SwooleProcessHandle (PID-based: liveness and signalling work, there is nothing to read or join()).

Method Returns Description
getPid() int the process PID
isAlive() bool is the process running now?
join(int $timeout = 0) ?int block until exit, draining stdout/stderr while waiting; exit code, null on timeout (seconds), or the recorded code / -1 if the resource is already gone
reap() bool non-blocking; drains stdout/stderr; true if finished (and reaped)
detach() void stop tracking; closes pipes, no proc_close, no transport cleanup (the child owns it)
getExitCode() ?int exit code once reaped; null after detach()
readOutput() string consuming STDOUT (bytes since the last call, non-blocking); '' if no output pipe
readError() string consuming STDERR (bytes since the last call, non-blocking); '' if no output pipe
signal(int $signal) bool posix_kill(pid, signal) — only if alive; false otherwise

reap(), detach(), and __destruct() are non-blocking on a live process; the blocking proc_close runs only on an already-dead process. __destruct() reaps a finished process, or detaches a still-running one — it never blocks the parent.


LaunchSpec (final readonly class)

Flytachi\Winter\Thread\LaunchSpec — an immutable bundle of every launch parameter. Build one directly to drive a launcher without a Thread.

php
new LaunchSpec(
  string  $payload,
  string  $namespace = '',
  string  $name = 'anonymous',
  ?string $tag = null,
  array   $arguments = [],
  bool    $debug = false,
  ?string $output = '/dev/null',
  bool    $detached = false,
)
Parameter Type Default Description
$payload string the already-serialized Runnable
$namespace string '' process-title grouping
$name string 'anonymous' process-title name
$tag ?string null instance tag
$arguments array<string, scalar|null> [] exposed to the task as --arg-*
$debug bool false child-side error reporting
$output ?string '/dev/null' '/dev/null' | file path | null (pipe to parent)
$detached bool false daemonize the child

All properties are public and readonly.


PayloadTransport (interface)

Flytachi\Winter\Thread\Payload\PayloadTransport — delivers the serialized payload across the process boundary in two halves plus a cleanup.

Method Returns Runs in Description
stage(string $payload) StagedPayload parent prepare delivery (fd-0 spec, CLI args, cleanup ref)
receive(array $options) string child read the payload back (STDIN, or shm via shmkey)
cleanup(StagedPayload $staged) void parent release staged resources; safe even if already gone

$options in receive() is array<string, mixed> — the parsed child CLI options.

Custom transports need a matching runner

The default AdaptiveRunner receives from STDIN or shm only — it does not call a custom transport’s receive(). A transport delivering out-of-band (Redis/TCP/FIFO) therefore also needs a matching child runner. See Swoole & Payload Delivery.

PipeTransport

…\Payload\PipeTransport — payload via the child’s stdin pipe (written after launch). No extension. Not Swoole-safe (uses a pipe fd). The default in plain CLI.

TempFileTransport

…\Payload\TempFileTransport — payload via a 0600 temp file placed on stdin, unlinked right after launch (child keeps its fd). No extension. Swoole-safe. Throws ThreadException if the temp file can’t be created/written.

ShmTransport

…\Payload\ShmTransport — payload via a 0600 System V shared-memory segment; the integer key is passed as --shmkey, and the child reads then deletes it. No disk, Swoole-safe. Requires ext-shmop — throws ThreadException ("ShmTransport requires ext-shmop.") on both stage() and receive() if missing, and on allocation failure.

StagedPayload (final readonly class)

Flytachi\Winter\Thread\Payload\StagedPayload — the result of stage(); the launcher reads it generically.

php
new StagedPayload(
  array   $stdinSpec,
  array   $cliArgs = [],
  ?string $pipePayload = null,
  ?string $unlinkAfterOpen = null,
  mixed   $ref = null,
)
Parameter Type Default Description
$stdinSpec array<int,string> proc_open fd-0 descriptor (['pipe','r'] or ['file',$path,'r'])
$cliArgs array<int,string> [] extra, already-safe launch args (e.g. ['--shmkey=123'])
$pipePayload ?string null written to the pipe after launch (pipe transport)
$unlinkAfterOpen ?string null unlinked after launch once the child holds its fd (temp-file transport)
$ref mixed null opaque cleanup handle (temp path / shm key) passed to cleanup()

Runner (interface)

Flytachi\Winter\Thread\Runner\Runner — child-side execution strategy.

php
public function execute(array $options): int
Parameter Type Description
$options array<string, mixed> parsed wRunner CLI options (namespace, name, tag, debug, detach, shmkey)

Returns int — the process exit code (0 success, non-zero failure).

AdaptiveRunner (final readonly class)

Flytachi\Winter\Thread\Runner\AdaptiveRunner — the default child-side runner (driven by wRunner). It depends only on a security provider — not on the launcher — so the two sides stay independent.

php
new AdaptiveRunner(
  ?DefaultSecurityProvider $security = null,
  mixed                    $errStream = null,
)
Parameter Type Default Description
$security ?DefaultSecurityProvider null verifies the payload signature; build it from the same secret the parent signed with (null = unsigned). wRunner builds it from WINTER_THREAD_SECRET
$errStream resource|null null where diagnostics are written; defaults to STDERR (injectable for tests)

execute() flow: receive payload (--shmkey → shm, else STDIN) → verify + deserialize via opis/closure (rejecting empty, non-Runnable, or unsigned/tampered payloads with a non-zero exit) → optional fork+setsid (detached) → set process title → run() → exit code.

Replacing the runner

To use a custom Runner, you replace the child bootstrap: write your own script like wRunner that constructs your runner (typically launched by a custom Launcher). There is no bindRunner() seam — by design, the child side is independent of the parent’s launcher. See Runner Lifecycle.


Signal (final class)

Flytachi\Winter\Thread\Signal — static POSIX helpers on a raw PID. isProcessRunning() treats zombies (state Z) as not running, on both Linux (/proc/<pid>/status) and macOS (ps).

Method Returns Description
isProcessRunning(int $pid) bool live (non-zombie) process with this PID exists?
interrupt(int $pid) bool send SIGINT (if running)
termination(int $pid) bool send SIGTERM (if running)
close(int $pid) bool send SIGHUP (if running)
kill(int $pid) bool send SIGKILL (if running)
wait(int $pid, int $timeout = 10) bool poll until gone; false on timeout (seconds)
interruptAndWait(int $pid, int $timeout = 10) bool SIGINT then wait
terminationAndWait(int $pid, int $timeout = 10) bool SIGTERM then wait
closeAndWait(int $pid, int $timeout = 10) bool SIGHUP then wait

No pause/resume on Signal

Signal covers only stop/terminate signals. Suspend/resume by raw PID is not exposed — use posix_kill($pid, SIGSTOP) / posix_kill($pid, SIGCONT) directly, or the Thread::pause()/resume() API.

PID reuse

Raw PIDs are subject to OS PID reuse — a PID may be reassigned after the original process terminates. Prefer a Thread/ProcessHandle for reliable tracking; use Signal only with freshly obtained PIDs (e.g. a detached worker’s self-reported PID). See PID Reuse & Signals.


ThreadException (class)

Flytachi\Winter\Thread\ThreadException extends \RuntimeException — the library’s only exception type.

Thrown on: launch failure (proc_open denied, or the process died immediately), starting an already-running Thread, misconfiguration (for example ShmTransport without ext-shmop), and transport staging failures (temp file / shm allocation). Catch it around start() and launcher setup.