Skip to content

Instantly share code, notes, and snippets.

@kandy
Created April 22, 2025 18:22
Show Gist options
  • Save kandy/a1073fa0ea1508dd9e28edafe94fa69c to your computer and use it in GitHub Desktop.
Save kandy/a1073fa0ea1508dd9e28edafe94fa69c to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
// Enable required extensions in php.ini: pcntl, posix, ffi
if (!extension_loaded('pcntl')) {
fwrite(STDERR, "FATAL tini (" . posix_getpid() . "): pcntl extension is not enabled.\n");
exit(1);
}
if (!extension_loaded('posix')) {
fwrite(STDERR, "FATAL tini (" . posix_getpid() . "): posix extension is not enabled.\n");
exit(1);
}
if (!extension_loaded('ffi')) {
fwrite(STDERR, "FATAL tini (" . posix_getpid() . "): ffi extension is not enabled.\n");
exit(1);
}
// Define FFI CDEF for necessary libc/syscall functions not in pcntl/posix
try {
$ffi = FFI::cdef(
"
// Standard C library functions
char* strerror(int errnum);
char* strsignal(int signum);
long int strtol(const char* nptr, char** endptr, int base);
void* calloc(size_t nmemb, size_t size);
void free(void* ptr);
void* memcpy(void* dest, const void* src, size_t n);
int errno; // Expose errno
// Process management
int setpgid(pid_t pid, pid_t pgid);
int tcsetpgrp(int fd, pid_t pgid);
pid_t getpgrp(void);
// prctl for parent death signal and subreaper
#define PR_SET_PDEATHSIG 4 // Common Linux prctl options
#define PR_SET_CHILD_SUBREAPER 36
#define PR_GET_CHILD_SUBREAPER 37
int prctl(int option, ...);
// Signal handling structures and functions (simplified for our use case)
// NOTE: These struct definitions are platform-dependent (Linux x86-64 common layout)
typedef struct {
char __val[16]; // Sufficiently large buffer for sigset_t on many systems
} sigset_t;
// Simplified siginfo_t - only need si_signo for sigtimedwait
// Full struct is union-heavy, this might be enough depending on layout
typedef struct {
int si_signo;
int si_errno;
int si_code;
// Placeholder for union members - size is critical!
// This needs to be big enough to match the C struct layout
// A common approach is to define a large enough buffer based on
// the expected size of the full union part. This is brittle.
// We'll use a placeholder that's likely large enough.
// A size of ~128-160 bytes for the full siginfo_t is common.
char _placeholder[150]; // Adjust if needed based on architecture/OS
} siginfo_t;
typedef long time_t;
typedef long suseconds_t; // Often long or int
typedef struct {
time_t tv_sec; // seconds
long tv_nsec; // nanoseconds
} timespec;
// sigaction definition (simplified, handler is const SIG_IGN)
typedef void (*__sigaction_handler_t)(int); // Function pointer type
typedef union sigval { int sival_int; void *sival_ptr; } sigval_t; // Used in siginfo_t, including for timers
// Structure mirroring glibc sigaction (simplified for SA_IGN)
struct sigaction {
__sigaction_handler_t sa_handler; // Will be SIG_IGN or SIG_DFL
unsigned long sa_flags; // Just an int is fine for flags
void (*sa_restorer)(void); // Not used when sa_handler is not a func
sigset_t sa_mask; // Signal mask
};
// These might be in <signal.h> or <bits/signalfd.h> etc.
// sigfillset and sigdelset are standard.
int sigfillset(sigset_t *set);
int sigdelset(sigset_t *set, int signum);
int sigemptyset(sigset_t *set); // Also useful
// sigtimedwait
// Note: The third arg struct timespec * is const
int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);
// Constants often needed
#define WNOHANG 1 // From wait.h
#define STDIN_FILENO 0 // From unistd.h
#define SIG_IGN ((__sigaction_handler_t) 1) // From signal.h
#define SIG_DFL ((__sigaction_handler_t) 0) // From signal.h
// Standard signals
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGCHLD 17 // SIGCLD is often same value but might differ
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGSYS 31 // Or 12 depending on arch/OS
// Exit codes for exec errors
#define ENOENT 2
#define EACCES 13
#define ESRCH 3 // No such process
#define EINVAL 22 // Invalid argument
#define EAGAIN 11 // Resource temporarily unavailable (for sigtimedwait)
#define EINTR 4 // Interrupted system call (for sigtimedwait)
#define ECHILD 10 // No child processes (for waitpid)
#define ENOTTY 25 // Not a tty
#define ENXIO 6 // No such device or address
",
"libc.so.6" // Or the appropriate libc library name on your system
);
} catch (FFI\Exception $e) {
fwrite(STDERR, "FATAL tini (" . posix_getpid() . "): Failed to load FFI definitions or libc: " . $e->getMessage() . "\n");
exit(1);
}
// -- PHP Equivalent Globals and Constants --
// Defined in tiniConfig.h (assumed)
const TINI_VERSION = "0.20.0"; // Replace with actual if known
const TINI_GIT = ""; // Replace with actual if known
const TINI_VERSION_STRING = "tini version " . TINI_VERSION . TINI_GIT;
// Defined in tiniLicense.h (assumed)
const LICENSE = "Tini License Text..."; // Replace with actual license text
const LICENSE_len = strlen(LICENSE);
// Signal mapping (subset, add others if needed)
const SIGNAL_NAMES = [
["name" => "SIGHUP", "number" => $ffi->SIGHUP],
["name" => "SIGINT", "number" => $ffi->SIGINT],
["name" => "SIGQUIT", "number" => $ffi->SIGQUIT],
["name" => "SIGILL", "number" => $ffi->SIGILL],
["name" => "SIGTRAP", "number" => $ffi->SIGTRAP],
["name" => "SIGABRT", "number" => $ffi->SIGABRT],
["name" => "SIGBUS", "number" => $ffi->SIGBUS],
["name" => "SIGFPE", "number" => $ffi->SIGFPE],
["name" => "SIGKILL", "number" => $ffi->SIGKILL],
["name" => "SIGUSR1", "number" => $ffi->SIGUSR1],
["name" => "SIGSEGV", "number" => $ffi->SIGSEGV],
["name" => "SIGUSR2", "number" => $ffi->SIGUSR2],
["name" => "SIGPIPE", "number" => $ffi->SIGPIPE],
["name" => "SIGALRM", "number" => $ffi->SIGALRM],
["name" => "SIGTERM", "number" => $ffi->SIGTERM],
["name" => "SIGCHLD", "number" => $ffi->SIGCHLD],
["name" => "SIGCONT", "number" => $ffi->SIGCONT],
["name" => "SIGSTOP", "number" => $ffi->SIGSTOP],
["name" => "SIGTSTP", "number" => $ffi->SIGTSTP],
["name" => "SIGTTIN", "number" => $ffi->SIGTTIN],
["name" => "SIGTTOU", "number" => $ffi->SIGTTOU],
["name" => "SIGURG", "number" => $ffi->SIGURG],
["name" => "SIGXCPU", "number" => $ffi->SIGXCPU],
["name" => "SIGXFSZ", "number" => $ffi->SIGXFSZ],
["name" => "SIGVTALRM", "number" => $ffi->SIGVTALRM],
["name" => "SIGPROF", "number" => $ffi->SIGPROF],
["name" => "SIGWINCH", "number" => $ffi->SIGWINCH],
["name" => "SIGSYS", "number" => $ffi->SIGSYS],
];
const STATUS_MAX = 255;
const STATUS_MIN = 0;
// Bitfield size for expect_status. (STATUS_MAX - STATUS_MIN + 1) = 256 statuses.
// 256 bits / 32 bits/int32 = 8 int32_t needed.
const EXPECT_STATUS_BITFIELD_SIZE = (STATUS_MAX - STATUS_MIN + 1) / 32;
$expectStatus = FFI::new("int32_t[" . EXPECT_STATUS_BITFIELD_SIZE . "]");
FFI::memset($expectStatus, 0, FFI::sizeof($expectStatus)); // Initialize to zero
// FFI helper macros translation
function INT32_BITFIELD_SET(\FFI\CData $field, int $i): void {
if ($i < STATUS_MIN || $i > STATUS_MAX) {
// Or handle error as appropriate
return;
}
$field[$i / 32] |= (1 << ($i % 32));
}
function INT32_BITFIELD_TEST(\FFI\CData $field, int $i): bool {
if ($i < STATUS_MIN || $i > STATUS_MAX) {
// Or handle error
return false;
}
return (bool)($field[$i / 32] & (1 << ($i % 32)));
}
const DEFAULT_VERBOSITY = 1; // Using non-minimal default
$verbosity = DEFAULT_VERBOSITY;
$subreaper = 0; // HAS_SUBREAPER is effectively always true on recent Linux
$parentDeathSignal = 0;
$killProcessGroup = 0;
$warnOnReap = 0;
// timeout for sigtimedwait { tv_sec = 1, tv_nsec = 0 }
$ts = FFI::new($ffi->timespec::class);
$ts->tv_sec = 1;
$ts->tv_nsec = 0;
// -- Logging Functions (PHP Equivalent) --
function get_pid(): int {
return posix_getpid();
}
function log_fatal(string $format, mixed ...$args): void {
$prefix = sprintf("[FATAL tini (%i)] ", get_pid());
$message = sprintf($format, ...$args);
fwrite(STDERR, $prefix . $message . "\n");
}
function log_warning(string $format, mixed ...$args): void {
global $verbosity;
if ($verbosity > 0) {
$prefix = sprintf("[WARN tini (%i)] ", get_pid());
$message = sprintf($format, ...$args);
fwrite(STDERR, $prefix . $message . "\n");
}
}
function log_info(string $format, mixed ...$args): void {
global $verbosity;
if ($verbosity > 1) {
$prefix = sprintf("[INFO tini (%i)] ", get_pid());
$message = sprintf($format, ...$args);
fwrite(STDOUT, $prefix . $message . "\n");
}
}
function log_debug(string $format, mixed ...$args): void {
global $verbosity;
if ($verbosity > 2) {
$prefix = sprintf("[DEBUG tini (%i)] ", get_pid());
$message = sprintf($format, ...$args);
fwrite(STDOUT, $prefix . $message . "\n");
}
}
function log_trace(string $format, mixed ...$args): void {
global $verbosity;
if ($verbosity > 3) {
$prefix = sprintf("[TRACE tini (%i)] ", get_pid());
$message = sprintf($format, ...$args);
fwrite(STDOUT, $prefix . $message . "\n");
}
}
// -- Signal Configuration Struct --
// This mirrors signal_configuration_t in C
// We pass pointers to the FFI CData objects
class SignalConfiguration {
public \FFI\CData $sigmask; // FFI\CData pointer to sigset_t
public \FFI\CData $sigttin_action; // FFI\CData pointer to struct sigaction
public \FFI\CData $sigttou_action; // FFI\CData pointer to struct sigaction
public function __construct() {
global $ffi;
$this->sigmask = FFI::new($ffi->sigset_t::class);
$this->sigttin_action = FFI::new($ffi->sigaction::class);
$this->sigttou_action = FFI::new($ffi->sigaction::class);
// Initialize the action structs to zero
FFI::memset($this->sigttin_action, 0, FFI::sizeof($this->sigttin_action));
FFI::memset($this->sigttou_action, 0, FFI::sizeof($this->sigttou_action));
}
public function __destruct() {
// FFI objects are usually cleaned up by PHP's GC, but explicit free
// is needed if they were allocated via FFI::new outside a managing object.
// In this case, the CData objects themselves hold the memory, which GC handles.
// No explicit free needed here unless we used FFI::new($ffi->sigset_t, true);
}
}
// -- Functions --
function restore_signals(SignalConfiguration $sigconf): int {
global $ffi;
// Use pcntl_sigprocmask which is a higher-level wrapper for sigprocmask
// SIG_SETMASK is POSIX define 2
if (!pcntl_sigprocmask(SIG_SETMASK, [], $oldsigset_out)) {
// Note: pcntl_sigprocmask's third argument is an *output* parameter
// The C code is restoring the *original* mask from before configure_signals.
// The C sigconf_ptr->sigmask_ptr held the *original* mask.
// Our configure_signals saves the original mask into the sigconf object's masks.
// Let's adjust configure_signals and restore_signals accordingly.
// Re-reading C: sigprocmask(SIG_SETMASK, sigconf_ptr->sigmask_ptr, NULL).
// The sigconf_ptr->sigmask_ptr in the C code *is* the original mask.
// So we need to pass the saved original mask here.
// Let's assume configure_signals saves the original mask into sigconf->sigmask
// and the original actions into sigconf->sigttin_action and sigconf->sigttou_action.
// We need to pass the *contents* of these FFI CData pointers.
// pcntl_sigprocmask expects a PHP array of signal numbers for the mask.
// Converting FFI sigset_t back to PHP array is not straightforward.
// We need to use FFI sigprocmask instead.
// Call FFI sigprocmask to restore the mask
if ($ffi->sigprocmask(SIG_SETMASK, $sigconf->sigmask, NULL)) {
log_fatal("Restoring child signal mask failed: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Restored signal mask");
}
// Use pcntl_sigaction to restore SIGTTIN and SIGTTOU handlers
// We need to pass the saved original struct sigaction contents.
// pcntl_sigaction expects handler as SIG_IGN, SIG_DFL or callable.
// It doesn't directly take the C struct.
// This requires using FFI sigaction to restore the *full* original struct.
// Call FFI sigaction to restore SIGTTIN handler
if ($ffi->sigaction(SIGTTIN, $sigconf->sigttin_action, NULL)) {
log_fatal("Restoring SIGTTIN handler failed: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Restored SIGTTIN handler");
// Call FFI sigaction to restore SIGTTOU handler
if ($ffi->sigaction(SIGTTOU, $sigconf->sigttou_action, NULL)) {
log_fatal("Restoring SIGTTOU handler failed: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Restored SIGTTOU handler");
return 0;
}
function isolate_child(): int {
global $ffi;
// Put the child into a new process group.
// setpgid(0, 0) means setpgid for the calling process (0) to its PID (0).
if ($ffi->setpgid(0, 0) < 0) {
log_fatal("setpgid failed: %s", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("setpgid(0, 0) successful");
// If there is a tty, allocate it to this new process group.
// tcsetpgrp(STDIN_FILENO, getpgrp())
$child_pgrp = $ffi->getpgrp();
if ($ffi->tcsetpgrp($ffi->STDIN_FILENO, $child_pgrp)) {
$errno = $ffi->errno;
if ($errno == $ffi->ENOTTY) {
log_debug("tcsetpgrp failed: no tty (ok to proceed)");
} else if ($errno == $ffi->ENXIO) {
// can occur on lx-branded zones
log_debug("tcsetpgrp failed: no such device (ok to proceed)");
} else {
log_fatal("tcsetpgrp failed: %s", $ffi->strerror($errno));
return 1;
}
} else {
log_trace("tcsetpgrp(%i, %i) successful", $ffi->STDIN_FILENO, $child_pgrp);
}
return 0;
}
function spawn(SignalConfiguration $sigconf, array $argv): ?int {
global $ffi;
// fork() - use pcntl_fork()
$pid = pcntl_fork();
if ($pid < 0) {
log_fatal("fork failed: %s", posix_strerror(posix_get_last_error()));
return 1;
} else if ($pid == 0) {
// Child process
// Put the child in a process group and make it the foreground process if there is a tty.
if (isolate_child()) {
exit(1); // Exit child on failure
}
// Restore all signal handlers to the way they were before we touched them.
if (restore_signals($sigconf)) {
exit(1); // Exit child on failure
}
// execvp(argv[0], argv) - use pcntl_exec()
// pcntl_exec expects an array for args
log_debug("Executing child: %s %s", $argv[0], implode(" ", array_slice($argv, 1)));
pcntl_exec($argv[0], array_slice($argv, 1));
// pcntl_exec will only return on an error
// Get errno via FFI
$errno = $ffi->errno;
// See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
$status = 1;
switch ($errno) {
case $ffi->ENOENT:
$status = 127;
break;
case $ffi->EACCES:
$status = 126;
break;
default:
// For other errors, use 1 by default
$status = 1;
break;
}
log_fatal("exec %s failed: %s", $argv[0], $ffi->strerror($errno));
exit($status); // Child exits with specific status
} else {
// Parent process
log_info("Spawned child process '%s' with pid '%i'", $argv[0], $pid);
// In PHP, we return the PID directly, no need for a pointer argument
return $pid;
}
}
function print_usage(string $name, $file): void {
global $subreaper;
global $ffi;
$basename = basename($name);
fwrite($file, sprintf("%s (%s)\n\n", $basename, TINI_VERSION_STRING));
// Minimal vs non-minimal usage string logic
// We assume non-minimal features are available if not TINI_MINIMAL is defined
// This PHP code is attempting a full conversion, so it includes all options
fwrite($file, sprintf("Usage: %s [OPTIONS] PROGRAM -- [ARGS] | --version\n\n", $basename));
fwrite($file, "Execute a program under the supervision of a valid init process (%s)\n\n", $basename);
fwrite($file, "Command line options:\n\n");
fwrite($file, " --version: Show version and exit.\n");
fwrite($file, " -h: Show this help message and exit.\n");
// HAS_SUBREAPER check in C is based on if PR_SET_CHILD_SUBREAPER is defined
// In FFI, we assume the constant is defined if we define it. We can check prctl availability later.
fwrite($file, sprintf(" -s: Register as a process subreaper (requires Linux >= 3.4).\n"));
fwrite($file, " -p SIGNAL: Trigger SIGNAL when parent dies, e.g. \"-p SIGKILL\".\n");
fwrite($file, " -v: Generate more verbose output. Repeat up to 3 times.\n");
fwrite($file, " -w: Print a warning when processes are getting reaped.\n");
fwrite($file, " -g: Send signals to the child's process group.\n");
fwrite($file, " -e EXIT_CODE: Remap EXIT_CODE (from 0 to 255) to 0 (can be repeated).\n");
fwrite($file, " -l: Show license and exit.\n");
fwrite($file, "\n");
fwrite($file, "Environment variables:\n\n");
fwrite($file, sprintf(" %s: Register as a process subreaper (requires Linux >= 3.4).\n", "TINI_SUBREAPER"));
fwrite($file, sprintf(" %s: Set the verbosity level (default: %d).\n", "TINI_VERBOSITY", DEFAULT_VERBOSITY));
fwrite($file, sprintf(" %s: Send signals to the child's process group.\n", "TINI_KILL_PROCESS_GROUP"));
fwrite($file, "\n");
}
function print_license($file): void {
fwrite($file, LICENSE);
}
function set_pdeathsig(string $arg): int {
global $parentDeathSignal;
global $ffi;
foreach (SIGNAL_NAMES as $sig_map) {
if ($sig_map["name"] === $arg) {
$parentDeathSignal = $sig_map["number"];
return 0;
}
}
return 1; // Not found
}
function add_expect_status(string $arg): int {
global $expectStatus;
global $ffi;
// strtol equivalent
$endptr = FFI::new("char*"); // FFI pointer for endptr
$status = $ffi->strtol($arg, $endptr, 10);
// Check if conversion was successful and entire string was consumed
if ($endptr === null || strlen(FFI::string($endptr)) > 0) {
return 1; // Conversion failed or extra characters
}
if (($status < STATUS_MIN) || ($status > STATUS_MAX)) {
return 1;
}
// Using PHP bitfield functions
INT32_BITFIELD_SET($expectStatus, $status);
return 0;
}
function parse_args(array $argv, int &$parse_fail_exitcode): ?array {
global $verbosity;
global $subreaper;
global $parentDeathSignal;
global $killProcessGroup;
global $warnOnReap;
global $expectStatus;
global $ffi; // Needed for FFI specific options like -e
$name = $argv[0];
$argc = count($argv);
// We handle --version if it's the *only* argument provided.
if ($argc === 2 && $argv[1] === "--version") {
$parse_fail_exitcode = 0;
fwrite(STDOUT, TINI_VERSION_STRING . "\n");
return null; // Indicates exit is needed
}
// Manual argument parsing to replace getopt
$child_args = [];
$parse_child_args = false;
$i = 1; // Start after program name
while ($i < $argc) {
$arg = $argv[$i];
if ($parse_child_args) {
$child_args[] = $arg;
} else {
switch ($arg) {
case "--":
// Stop parsing options, rest are child args
$parse_child_args = true;
break;
case "-h":
print_usage($name, STDOUT);
$parse_fail_exitcode = 0;
return null; // Indicates exit
case "-s":
$subreaper++;
break;
case "-p":
$i++; // Move to next arg for the value
if ($i >= $argc) {
log_fatal("Option -p requires an argument.");
print_usage($name, STDERR);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
if (set_pdeathsig($argv[$i])) {
log_fatal("Not a valid option for -p: %s", $argv[$i]);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
break;
case "-v":
$verbosity++;
break;
case "-w":
$warnOnReap++;
break;
case "-g":
$killProcessGroup++;
break;
case "-e":
$i++; // Move to next arg for the value
if ($i >= $argc) {
log_fatal("Option -e requires an argument.");
print_usage($name, STDERR);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
if (add_expect_status($argv[$i])) {
log_fatal("Not a valid option for -e: %s", $argv[$i]);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
break;
case "-l":
print_license(STDOUT);
$parse_fail_exitcode = 0;
return null; // Indicates exit
case "--version":
// Handled at the beginning if it's the *only* arg.
// If it's here, it's misplaced. Treat as error or just ignore?
// Tini C treats it only at the beginning. Let's do the same.
log_fatal("--version must be the only argument.");
$parse_fail_exitcode = 1;
return null; // Indicates failure
default:
// If it starts with '-', it's an unknown option
if (str_starts_with($arg, '-')) {
log_fatal("Unknown option: %s", $arg);
print_usage($name, STDERR);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
// Otherwise, this is the program name and the rest are its args
$child_args[] = $arg;
$parse_child_args = true; // The rest are implicitly args
break;
}
}
$i++;
}
if (empty($child_args)) {
/* User forgot to provide args! */
print_usage($name, STDERR);
$parse_fail_exitcode = 1;
return null; // Indicates failure
}
// Return the child args array
return $child_args;
}
function parse_env(): void {
global $subreaper;
global $killProcessGroup;
global $verbosity;
// Subreaper from environment
if (getenv("TINI_SUBREAPER") !== false) {
$subreaper++;
}
// Kill process group from environment
if (getenv("TINI_KILL_PROCESS_GROUP") !== false) {
$killProcessGroup++;
}
// Verbosity from environment
$envVerbosity = getenv("TINI_VERBOSITY");
if ($envVerbosity !== false) {
$verbosity = intval($envVerbosity);
}
}
function register_subreaper(): int {
global $subreaper;
global $ffi;
if ($subreaper > 0) {
// prctl(PR_SET_CHILD_SUBREAPER, 1)
if ($ffi->prctl($ffi->PR_SET_CHILD_SUBREAPER, 1)) {
$errno = $ffi->errno;
if ($errno == $ffi->EINVAL) {
log_fatal("PR_SET_CHILD_SUBREAPER is unavailable on this platform. Are you using Linux >= 3.4?");
} else {
log_fatal("Failed to register as child subreaper: %s", $ffi->strerror($errno));
}
return 1;
} else {
log_trace("Registered as child subreaper");
}
}
return 0;
}
function reaper_check(): void {
global $warnOnReap;
global $ffi;
$reaper_warning = "Tini is not running as PID 1 "
. ($subreaper > 0 ? "" : "and isn't registered as a child subreaper")
. ".\n"
. "Zombie processes will not be re-parented to Tini, so zombie reaping won't work.\n"
. "To fix the problem, "
. (($subreaper > 0) ? sprintf("use the -s option or set the environment variable %s to register Tini as a child subreaper, or ", "TINI_SUBREAPER") : "")
. "run Tini as PID 1.";
/* Check that we can properly reap zombies */
if (posix_getpid() == 1) {
return;
}
// Check PR_GET_CHILD_SUBREAPER
$bit = FFI::new('int'); // Need a pointer to pass to prctl for GET operations
if ($ffi->prctl($ffi->PR_GET_CHILD_SUBREAPER, FFI::addr($bit))) {
log_debug("Failed to read child subreaper attribute: %s", $ffi->strerror($ffi->errno));
} else if ($bit->cdata == 1) {
// Already a subreaper, don't warn
return;
}
// If we are here, we are not PID 1 and not a subreaper
if ($warnOnReap > 0) {
log_warning("%s", $reaper_warning);
} else {
log_debug("%s", $reaper_warning); // Log as debug if no warning requested
}
}
function configure_signals(\FFI\CData &$parent_sigset, SignalConfiguration $sigconf): int {
global $ffi;
/* Block all signals that are meant to be collected by the main loop */
// sigfillset fills the set with all signals
if ($ffi->sigfillset($parent_sigset)) {
log_fatal("sigfillset failed: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("parent_sigset filled with all signals");
// These ones shouldn't be collected by the main loop - remove from parent_sigset
$signalsForTini = [$ffi->SIGFPE, $ffi->SIGILL, $ffi->SIGSEGV, $ffi->SIGBUS, $ffi->SIGABRT, $ffi->SIGTRAP, $ffi->SIGSYS, $ffi->SIGTTIN, $ffi->SIGTTOU];
foreach ($signalsForTini as $signal) {
if ($ffi->sigdelset($parent_sigset, $signal)) {
log_fatal("sigdelset failed for signal '%i': '%s'", $signal, $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Removed signal %i from parent_sigset", $signal);
}
// Block these signals in the parent process and save the old mask
// sigprocmask(SIG_SETMASK, &parent_sigset, sigconf->sigmask_ptr)
// SIG_SETMASK is POSIX define 2
// The third argument (sigconf->sigmask_ptr) is where the *old* mask is saved.
if ($ffi->sigprocmask(SIG_SETMASK, $parent_sigset, $sigconf->sigmask)) {
log_fatal("sigprocmask failed: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Blocked signals in parent and saved original mask");
// Handle SIGTTIN and SIGTTOU separately by ignoring them.
// Use FFI sigaction to save the original action and set the new one.
// The new action is SIG_IGN.
$ign_action = FFI::new($ffi->sigaction::class);
FFI::memset($ign_action, 0, FFI::sizeof($ign_action)); // Initialize to zero
$ign_action->sa_handler = $ffi->SIG_IGN; // Set handler to ignore
$ffi->sigemptyset($ign_action->sa_mask); // Empty mask for the action (signals blocked *while* handler runs) - not strictly needed for SIG_IGN
// sigaction(SIGTTIN, &ign_action, sigconf->sigttin_action_ptr)
// The third argument is where the *old* action is saved.
if ($ffi->sigaction(SIGTTIN, $ign_action, $sigconf->sigttin_action)) {
log_fatal("Failed to ignore SIGTTIN: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Ignored SIGTTIN and saved original handler");
// sigaction(SIGTTOU, &ign_action, sigconf->sigttou_action_ptr)
// The third argument is where the *old* action is saved.
if ($ffi->sigaction(SIGTTOU, $ign_action, $sigconf->sigttou_action)) {
log_fatal("Failed to ignore SIGTTOU: '%s'", $ffi->strerror($ffi->errno));
return 1;
}
log_trace("Ignored SIGTTOU and saved original handler");
return 0;
}
function wait_and_forward_signal(\FFI\CData $parent_sigset, int $child_pid): int {
global $ffi;
global $ts; // The timeout struct
global $killProcessGroup;
// sigtimedwait needs a siginfo_t struct to fill
$siginfo = FFI::new($ffi->siginfo_t::class);
FFI::memset($siginfo, 0, FFI::sizeof($siginfo)); // Initialize to zero
// sigtimedwait returns the signal number on success, -1 on error/timeout
$signo = $ffi->sigtimedwait($parent_sigset, $siginfo, FFI::addr($ts));
if ($signo == -1) {
$errno = $ffi->errno;
switch ($errno) {
case $ffi->EAGAIN: // Timeout occurred
// log_trace("sigtimedwait timeout (EAGAIN)"); // Too noisy
break;
case $ffi->EINTR: // Interrupted by an unblocked signal (shouldn't happen with our mask, but good practice)
log_trace("sigtimedwait interrupted (EINTR)");
break;
default:
log_fatal("Unexpected error in sigtimedwait: '%s'", $ffi->strerror($errno));
return 1;
}
} else {
/* There is a signal to handle here */
$received_signal = $siginfo->si_signo; // Get the signal number from the struct
log_trace("Received signal: '%s' (%i)", $ffi->strsignal($received_signal), $received_signal);
switch ($received_signal) {
case $ffi->SIGCHLD:
/* Special-cased, as we don't forward SIGCHLD. Instead, we'll
* fallthrough to reaping processes.
*/
// log_trace("Received SIGCHLD (will reap)"); // Done above
break;
default:
// Forward anything else
// Use posix_kill which handles negative PIDs for process groups
$target_pid = $killProcessGroup ? -$child_pid : $child_pid;
log_debug("Forwarding signal '%s' (%i) to %s '%i'",
$ffi->strsignal($received_signal), $received_signal,
$killProcessGroup ? "process group" : "process", $target_pid);
if (!posix_kill($target_pid, $received_signal)) {
$errno = posix_get_last_error(); // Get errno from posix extension call
if ($errno == $ffi->ESRCH) {
log_warning("Child was dead when forwarding signal '%s'", $ffi->strsignal($received_signal));
} else {
log_fatal("Unexpected error when forwarding signal '%s' (%i): '%s'",
$ffi->strsignal($received_signal), $received_signal,
posix_strerror($errno));
return 1;
}
}
break;
}
}
return 0;
}
function reap_zombies(int $child_pid, ?int &$child_exitcode): int {
global $warnOnReap;
global $expectStatus;
global $ffi;
// Use pcntl_waitpid for waiting
$status = 0; // Output status value
$options = $ffi->WNOHANG; // Non-blocking wait
while (true) {
// waitpid(-1, &current_status, WNOHANG)
// -1 means wait for any child process
$current_pid = pcntl_waitpid(-1, $status, $options);
if ($current_pid == -1) {
$errno = posix_get_last_error(); // Get errno from posix extension
if ($errno == $ffi->ECHILD) {
log_trace("No child to wait (ECHILD)");
break; // No more children
}
log_fatal("Error while waiting for pids: '%s'", posix_strerror($errno));
return 1;
} else if ($current_pid == 0) {
log_trace("No child to reap (WNOHANG)");
break; // No children ready to be reaped
} else {
/* A child was reaped. Check whether it's the main one. */
log_debug("Reaped child with pid: '%i'", $current_pid);
if ($current_pid == $child_pid) {
if (pcntl_wifexited($status)) {
/* Our process exited normally. */
$exit_status = pcntl_wexitstatus($status);
log_info("Main child exited normally (with status '%i')", $exit_status);
$child_exitcode = $exit_status;
} else if (pcntl_wifsignaled($status)) {
/* Our process was terminated. Emulate what sh / bash
* would do, which is to return 128 + signal number.
*/
$term_signal = pcntl_wtermsig($status);
log_info("Main child exited with signal (with signal '%s')", $ffi->strsignal($term_signal));
$child_exitcode = 128 + $term_signal;
} else {
log_fatal("Main child exited for unknown reason");
return 1;
}
// Be safe, ensure the status code is indeed between 0 and 255.
$child_exitcode = $child_exitcode % (STATUS_MAX - STATUS_MIN + 1);
// If this exitcode was remapped, then set it to 0.
if (INT32_BITFIELD_TEST($expectStatus, $child_exitcode)) {
$child_exitcode = 0;
log_debug("Exit code %i remapped to 0", $child_exitcode);
}
} else if ($warnOnReap > 0) {
log_warning("Reaped zombie process with pid=%i", $current_pid);
}
// Check if other children have been reaped. Continue the loop.
continue;
}
/* If we make it here, that's because we did not continue in the loop. */
break;
}
return 0;
}
// -- Main Execution Block --
// php $argv contains script name as first element
$php_argv = $argv;
$php_argc = count($php_argv);
$child_pid = null; // Will store child PID
$child_exitcode = null; // Will store child exit code, null means not exited yet
$parse_exitcode = 1; // Default exit code for parsing failures
/* Parse command line arguments */
// parse_args returns the child arguments array or null if exit/failure is needed
$child_args = parse_args($php_argv, $parse_exitcode);
if ($child_args === null) {
// parse_args returned null, indicating it handled output and exit code
exit($parse_exitcode);
}
/* Parse environment */
parse_env();
/* Configure signals */
// Need FFI CData objects for signal sets and actions
$parent_sigset = FFI::new($ffi->sigset_t::class); // Used in parent loop
$sigconf = new SignalConfiguration(); // Holds original mask and actions
if (configure_signals($parent_sigset, $sigconf)) {
exit(1);
}
/* Trigger signal on this process when the parent process exits. */
if ($parentDeathSignal > 0) {
// prctl(PR_SET_PDEATHSIG, parent_death_signal)
if ($ffi->prctl($ffi->PR_SET_PDEATHSIG, $parentDeathSignal)) {
log_fatal("Failed to set up parent death signal: '%s'", $ffi->strerror($ffi->errno));
exit(1);
}
log_trace("Set parent death signal to %i", $parentDeathSignal);
}
/* If available and requested, register as a subreaper */
if (register_subreaper()) {
exit(1);
};
/* Are we going to reap zombies properly? If not, warn. */
reaper_check();
/* Go on */
// spawn returns child PID on success, or exit code on failure (handled inside)
$spawn_result = spawn($sigconf, $child_args);
if ($spawn_result !== null) {
// Spawn successful, $spawn_result is child PID
$child_pid = $spawn_result;
} else {
// Spawn failed (child exited), spawn function already printed errors and exited.
// The parent should also exit here if the child couldn't even start.
// Note: The C code's spawn returns an exit status from the *parent's* perspective.
// If fork or execvp failed, the C parent returns 1 or a specific exec error code.
// Let's adjust the PHP spawn to return an error code *to the parent* if fork/exec setup fails.
// Re-checking C: spawn returns 1 on fork failure, or the execvp error status on exec failure (in child, causing child exit).
// The current PHP spawn returns PID on success, or child exits on failure.
// We need the parent to know if spawn setup failed *before* exec.
// Let's make PHP spawn return -1 on parent-side setup failure (fork). Child-side exec failure results in child exit.
// Reworking PHP spawn to return PID or -1 on fork error:
// If fork() fails, pcntl_fork returns -1. We handle this.
// If fork() succeeds (pid == 0), child runs, attempts isolate/restore/exec. If any fail, child exits.
// If fork() succeeds (pid > 0), parent continues. $child_pid is set to PID.
// The logic is already mostly correct, just need to catch the -1 from pcntl_fork if needed.
// Our current spawn returns PID or exits *in the child*. Let's return PID or false on parent-side failure.
// Let's adjust spawn to return child_pid (int > 0) on success, or 0 on parent-side failure (e.g., fork fails).
// Child-side exec failures cause the child to exit, which is handled later by reap_zombies.
// The return type hint ?int allows null or int. Let's return null on failure.
// Retrying the spawn call and checking its return:
unset($spawn_result); // Clear previous result
$spawn_result = spawn($sigconf, $child_args); // spawn now returns PID or null on parent-side error
if ($spawn_result === null) {
// Parent-side spawn setup failed (e.g., fork error)
exit(1); // Exit parent with error
}
$child_pid = $spawn_result;
// In the C code, child_args_ptr was free'd here.
// In PHP, $child_args array is managed by GC. No explicit free needed for the array.
// If we used FFI::new for the child args array, we would free it here.
// Our current implementation uses a PHP array, so no explicit FFI::free.
}
// Main loop: wait for signals/children, forward signals, reap zombies
while (true) {
// Wait for one signal (or timeout), and forward it if not SIGCHLD
if (wait_and_forward_signal($parent_sigset, $child_pid)) {
exit(1); // Exit on fatal error in signal handling
}
/* Now, reap zombies */
// reap_zombies updates $child_exitcode by reference if the main child exited.
if (reap_zombies($child_pid, $child_exitcode)) {
exit(1); // Exit on fatal error in reaping
}
// Check if the main child has exited
if ($child_exitcode !== null) { // null means child hasn't been reaped yet
log_trace("Exiting: child has exited with status %s", $child_exitcode);
exit($child_exitcode); // Exit with the child's status (potentially remapped)
}
}
// This point should theoretically not be reached
exit(1); // Should not happen, but as a safeguard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment