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.
public function run(array $args): voidPut 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
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
start(
array $arguments = [],
bool $debugMode = false,
?string $outputTarget = '/dev/null',
bool $detached = false,
): intSerializes 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 bool — true 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:
Thread::bindLauncher(CliLauncher::adaptive(secret: '…'));
Thread::launcher(); // the current launcher; an AdaptiveLauncher when none is boundLow-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.
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.
new AdaptiveLauncher(CliLauncher $cli, SwooleLauncher $swoole)
AdaptiveLauncher::adaptive(
?string $secret = null,
?string $binaryPath = null,
?string $runnerPath = null,
): AdaptiveLauncherCliLauncher (final readonly class)
Flytachi\Winter\Thread\Launch\CliLauncher — the CLI and FPM backend, proc_open-based. The command is built fully escapeshellarg-escaped.
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().
new SwooleLauncher(string $binaryPath, string $runnerPath, ?string $secret = null)
SwooleLauncher::adaptive(
?string $secret = null,
?string $binaryPath = null,
?string $runnerPath = null,
): SwooleLauncherThere 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.
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.
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.
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.
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.