Created
April 22, 2025 18:22
-
-
Save kandy/a1073fa0ea1508dd9e28edafe94fa69c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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, ¤t_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