123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641 |
- <?php
- namespace FPM;
- use Adoy\FastCGI\Client;
- require_once 'fcgi.inc';
- require_once 'logreader.inc';
- require_once 'logtool.inc';
- require_once 'response.inc';
- class Tester
- {
- /**
- * Config directory for included files.
- */
- const CONF_DIR = __DIR__ . '/conf.d';
- /**
- * File extension for access log.
- */
- const FILE_EXT_LOG_ACC = 'acc.log';
- /**
- * File extension for error log.
- */
- const FILE_EXT_LOG_ERR = 'err.log';
- /**
- * File extension for slow log.
- */
- const FILE_EXT_LOG_SLOW = 'slow.log';
- /**
- * File extension for PID file.
- */
- const FILE_EXT_PID = 'pid';
- /**
- * @var array
- */
- static private array $supportedFiles = [
- self::FILE_EXT_LOG_ACC,
- self::FILE_EXT_LOG_ERR,
- self::FILE_EXT_LOG_SLOW,
- self::FILE_EXT_PID,
- 'src.php',
- 'ini',
- 'skip.ini',
- '*.sock',
- ];
- /**
- * @var array
- */
- static private array $filesToClean = ['.user.ini'];
- /**
- * @var bool
- */
- private bool $debug;
- /**
- * @var array
- */
- private array $clients = [];
- /**
- * @var LogReader
- */
- private LogReader $logReader;
- /**
- * @var LogTool
- */
- private LogTool $logTool;
- /**
- * Configuration template
- *
- * @var string|array
- */
- private string|array $configTemplate;
- /**
- * The PHP code to execute
- *
- * @var string
- */
- private string $code;
- /**
- * @var array
- */
- private array $options;
- /**
- * @var string
- */
- private string $fileName;
- /**
- * @var resource
- */
- private $masterProcess;
- /**
- * @var resource
- */
- private $outDesc;
- /**
- * @var array
- */
- private array $ports = [];
- /**
- * @var string|null
- */
- private ?string $error = null;
- /**
- * The last response for the request call
- *
- * @var Response|null
- */
- private ?Response $response;
- /**
- * Clean all the created files up
- *
- * @param int $backTraceIndex
- */
- static public function clean($backTraceIndex = 1)
- {
- $filePrefix = self::getCallerFileName($backTraceIndex);
- if (str_ends_with($filePrefix, 'clean.')) {
- $filePrefix = substr($filePrefix, 0, -6);
- }
- $filesToClean = array_merge(
- array_map(
- function ($fileExtension) use ($filePrefix) {
- return $filePrefix . $fileExtension;
- },
- self::$supportedFiles
- ),
- array_map(
- function ($fileExtension) {
- return __DIR__ . '/' . $fileExtension;
- },
- self::$filesToClean
- )
- );
- // clean all the root files
- foreach ($filesToClean as $filePattern) {
- foreach (glob($filePattern) as $filePath) {
- unlink($filePath);
- }
- }
- self::cleanConfigFiles();
- }
- /**
- * Clean config files
- */
- static public function cleanConfigFiles()
- {
- if (is_dir(self::CONF_DIR)) {
- foreach (glob(self::CONF_DIR . '/*.conf') as $name) {
- unlink($name);
- }
- rmdir(self::CONF_DIR);
- }
- }
- /**
- * @param int $backTraceIndex
- *
- * @return string
- */
- static private function getCallerFileName(int $backTraceIndex = 1): string
- {
- $backtrace = debug_backtrace();
- if (isset($backtrace[$backTraceIndex]['file'])) {
- $filePath = $backtrace[$backTraceIndex]['file'];
- } else {
- $filePath = __FILE__;
- }
- return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION)));
- }
- /**
- * @return bool|string
- */
- static public function findExecutable(): bool|string
- {
- $phpPath = getenv("TEST_PHP_EXECUTABLE");
- for ($i = 0; $i < 2; $i++) {
- $slashPosition = strrpos($phpPath, "/");
- if ($slashPosition) {
- $phpPath = substr($phpPath, 0, $slashPosition);
- } else {
- break;
- }
- }
- if ($phpPath && is_dir($phpPath)) {
- if (file_exists($phpPath . "/fpm/php-fpm") && is_executable($phpPath . "/fpm/php-fpm")) {
- /* gotcha */
- return $phpPath . "/fpm/php-fpm";
- }
- $phpSbinFpmi = $phpPath . "/sbin/php-fpm";
- if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) {
- return $phpSbinFpmi;
- }
- }
- // try local php-fpm
- $fpmPath = dirname(__DIR__) . '/php-fpm';
- if (file_exists($fpmPath) && is_executable($fpmPath)) {
- return $fpmPath;
- }
- return false;
- }
- /**
- * Skip test if any of the supplied files does not exist.
- *
- * @param mixed $files
- */
- static public function skipIfAnyFileDoesNotExist($files)
- {
- if ( ! is_array($files)) {
- $files = array($files);
- }
- foreach ($files as $file) {
- if ( ! file_exists($file)) {
- die("skip File $file does not exist");
- }
- }
- }
- /**
- * Skip test if config file is invalid.
- *
- * @param string $configTemplate
- *
- * @throws \Exception
- */
- static public function skipIfConfigFails(string $configTemplate)
- {
- $tester = new self($configTemplate, '', [], self::getCallerFileName());
- $testResult = $tester->testConfig();
- if ($testResult !== null) {
- self::clean(2);
- die("skip $testResult");
- }
- }
- /**
- * Skip test if IPv6 is not supported.
- */
- static public function skipIfIPv6IsNotSupported()
- {
- @stream_socket_client('tcp://[::1]:0', $errno);
- if ($errno != 111) {
- die('skip IPv6 is not supported.');
- }
- }
- /**
- * Skip if running on Travis.
- *
- * @param $message
- */
- static public function skipIfTravis($message)
- {
- if (getenv("TRAVIS")) {
- die('skip Travis: ' . $message);
- }
- }
- /**
- * Skip if not running as root.
- */
- static public function skipIfNotRoot()
- {
- if (getmyuid() != 0) {
- die('skip not running as root');
- }
- }
- /**
- * Skip if running as root.
- */
- static public function skipIfRoot()
- {
- if (getmyuid() == 0) {
- die('skip running as root');
- }
- }
- /**
- * Skip if posix extension not loaded.
- */
- static public function skipIfPosixNotLoaded()
- {
- if ( ! extension_loaded('posix')) {
- die('skip posix extension not loaded');
- }
- }
- /**
- * Tester constructor.
- *
- * @param string|array $configTemplate
- * @param string $code
- * @param array $options
- * @param string|null $fileName
- * @param bool|null $debug
- */
- public function __construct(
- string|array $configTemplate,
- string $code = '',
- array $options = [],
- string $fileName = null,
- bool $debug = null
- ) {
- $this->configTemplate = $configTemplate;
- $this->code = $code;
- $this->options = $options;
- $this->fileName = $fileName ?: self::getCallerFileName();
- $this->debug = $debug !== null ? $debug : (bool)getenv('TEST_FPM_DEBUG');
- $this->logReader = new LogReader($this->debug);
- $this->logTool = new LogTool($this->logReader, $this->debug);
- }
- /**
- * @param string $ini
- */
- public function setUserIni(string $ini)
- {
- $iniFile = __DIR__ . '/.user.ini';
- $this->trace('Setting .user.ini file', $ini, isFile: true);
- file_put_contents($iniFile, $ini);
- }
- /**
- * Test configuration file.
- *
- * @return null|string
- * @throws \Exception
- */
- public function testConfig()
- {
- $configFile = $this->createConfig();
- $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1';
- $this->trace('Testing config using command', $cmd, true);
- exec($cmd, $output, $code);
- if ($code) {
- return preg_replace("/\[.+?\]/", "", $output[0]);
- }
- return null;
- }
- /**
- * Start PHP-FPM master process
- *
- * @param array $extraArgs Command extra arguments.
- * @param bool $forceStderr Whether to output to stderr so error log is used.
- *
- * @return bool
- * @throws \Exception
- */
- public function start(array $extraArgs = [], bool $forceStderr = true)
- {
- $configFile = $this->createConfig();
- $desc = $this->outDesc ? [] : [1 => array('pipe', 'w'), 2 => array('redirect', 1)];
- $cmd = [self::findExecutable(), '-F', '-y', $configFile];
- if ($forceStderr) {
- $cmd[] = '-O';
- }
- if (getenv('TEST_FPM_RUN_AS_ROOT')) {
- $cmd[] = '--allow-to-run-as-root';
- }
- $cmd = array_merge($cmd, $extraArgs);
- $this->trace('Starting FPM using command:', $cmd, true);
- $this->masterProcess = proc_open($cmd, $desc, $pipes);
- register_shutdown_function(
- function ($masterProcess) use ($configFile) {
- @unlink($configFile);
- if (is_resource($masterProcess)) {
- @proc_terminate($masterProcess);
- while (proc_get_status($masterProcess)['running']) {
- usleep(10000);
- }
- }
- },
- $this->masterProcess
- );
- if ( ! $this->outDesc !== false) {
- $this->outDesc = $pipes[1];
- $this->logReader->setStreamSource('{{MASTER:OUT}}', $this->outDesc);
- }
- return true;
- }
- /**
- * Run until needle is found in the log.
- *
- * @param string $pattern Search pattern to find.
- *
- * @return bool
- * @throws \Exception
- */
- public function runTill(string $pattern)
- {
- $this->start();
- $found = $this->logTool->expectPattern($pattern);
- $this->close(true);
- return $found;
- }
- /**
- * Check if connection works.
- *
- * @param string $host
- * @param string|null $successMessage
- * @param string|null $errorMessage
- * @param int $attempts
- * @param int $delay
- */
- public function checkConnection(
- string $host = '127.0.0.1',
- string $successMessage = null,
- ?string $errorMessage = 'Connection failed',
- int $attempts = 20,
- int $delay = 50000
- ) {
- $i = 0;
- do {
- if ($i > 0 && $delay > 0) {
- usleep($delay);
- }
- $fp = @fsockopen($host, $this->getPort());
- } while ((++$i < $attempts) && ! $fp);
- if ($fp) {
- $this->trace('Checking connection successful');
- $this->message($successMessage);
- fclose($fp);
- } else {
- $this->message($errorMessage);
- }
- }
- /**
- * Execute request with parameters ordered for better checking.
- *
- * @param string $address
- * @param string|null $successMessage
- * @param string|null $errorMessage
- * @param string $uri
- * @param string $query
- * @param array $headers
- *
- * @return Response
- */
- public function checkRequest(
- string $address,
- string $successMessage = null,
- string $errorMessage = null,
- string $uri = '/ping',
- string $query = '',
- array $headers = []
- ): Response {
- return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage);
- }
- /**
- * Execute and check ping request.
- *
- * @param string $address
- * @param string $pingPath
- * @param string $pingResponse
- */
- public function ping(
- string $address = '{{ADDR}}',
- string $pingResponse = 'pong',
- string $pingPath = '/ping'
- ) {
- $response = $this->request('', [], $pingPath, $address);
- $response->expectBody($pingResponse, 'text/plain');
- }
- /**
- * Execute and check status request(s).
- *
- * @param array $expectedFields
- * @param string|null $address
- * @param string $statusPath
- * @param mixed $formats
- *
- * @throws \Exception
- */
- public function status(
- array $expectedFields,
- string $address = null,
- string $statusPath = '/status',
- $formats = ['plain', 'html', 'xml', 'json', 'openmetrics']
- ) {
- if ( ! is_array($formats)) {
- $formats = [$formats];
- }
- require_once "status.inc";
- $status = new Status();
- foreach ($formats as $format) {
- $query = $format === 'plain' ? '' : $format;
- $response = $this->request($query, [], $statusPath, $address);
- $status->checkStatus($response, $expectedFields, $format);
- }
- }
- /**
- * Get request params array.
- *
- * @param string $query
- * @param array $headers
- * @param string|null $uri
- * @param string|null $scriptFilename
- *
- * @return array
- */
- private function getRequestParams(
- string $query = '',
- array $headers = [],
- string $uri = null,
- string $scriptFilename = null
- ): array {
- if (is_null($uri)) {
- $uri = $this->makeSourceFile();
- }
- $params = array_merge(
- [
- 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
- 'REQUEST_METHOD' => 'GET',
- 'SCRIPT_FILENAME' => $scriptFilename ?: $uri,
- 'SCRIPT_NAME' => $uri,
- 'QUERY_STRING' => $query,
- 'REQUEST_URI' => $uri . ($query ? '?' . $query : ""),
- 'DOCUMENT_URI' => $uri,
- 'SERVER_SOFTWARE' => 'php/fcgiclient',
- 'REMOTE_ADDR' => '127.0.0.1',
- 'REMOTE_PORT' => '7777',
- 'SERVER_ADDR' => '127.0.0.1',
- 'SERVER_PORT' => '80',
- 'SERVER_NAME' => php_uname('n'),
- 'SERVER_PROTOCOL' => 'HTTP/1.1',
- 'DOCUMENT_ROOT' => __DIR__,
- 'CONTENT_TYPE' => '',
- 'CONTENT_LENGTH' => 0
- ],
- $headers
- );
- return array_filter($params, function ($value) {
- return ! is_null($value);
- });
- }
- /**
- * Execute request.
- *
- * @param string $query
- * @param array $headers
- * @param string|null $uri
- * @param string|null $address
- * @param string|null $successMessage
- * @param string|null $errorMessagereadLimit
- * @param bool $connKeepAlive
- * @param string|null $scriptFilename = null
- * @param bool $expectError
- * @param int $readLimit
- *
- * @return Response
- */
- public function request(
- string $query = '',
- array $headers = [],
- string $uri = null,
- string $address = null,
- string $successMessage = null,
- string $errorMessage = null,
- bool $connKeepAlive = false,
- string $scriptFilename = null,
- bool $expectError = false,
- int $readLimit = -1,
- ): Response {
- if ($this->hasError()) {
- return new Response(null, true);
- }
- $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename);
- $this->trace('Request params', $params);
- try {
- $this->response = new Response(
- $this->getClient($address, $connKeepAlive)->request_data($params, false, $readLimit)
- );
- if ($expectError) {
- $this->error('Expected request error but the request was successful');
- } else {
- $this->message($successMessage);
- }
- } catch (\Exception $exception) {
- if ($expectError) {
- $this->message($successMessage);
- } elseif ($errorMessage === null) {
- $this->error("Request failed", $exception);
- } else {
- $this->message($errorMessage);
- }
- $this->response = new Response();
- }
- if ($this->debug) {
- $this->response->debugOutput();
- }
- return $this->response;
- }
- /**
- * Execute multiple requests in parallel.
- *
- * @param int|array $requests
- * @param string|null $address
- * @param string|null $successMessage
- * @param string|null $errorMessage
- * @param bool $connKeepAlive
- * @param int $readTimeout
- *
- * @return Response[]
- * @throws \Exception
- */
- public function multiRequest(
- int|array $requests,
- string $address = null,
- string $successMessage = null,
- string $errorMessage = null,
- bool $connKeepAlive = false,
- int $readTimeout = 0
- ) {
- if (is_numeric($requests)) {
- $requests = array_fill(0, $requests, []);
- }
- if ($this->hasError()) {
- return array_map(fn($request) => new Response(null, true), $requests);
- }
- try {
- $connections = array_map(function ($requestData) use ($address, $connKeepAlive) {
- $client = $this->getClient($address, $connKeepAlive);
- $params = $this->getRequestParams(
- $requestData['query'] ?? '',
- $requestData['headers'] ?? [],
- $requestData['uri'] ?? null
- );
- return [
- 'client' => $client,
- 'requestId' => $client->async_request($params, false),
- ];
- }, $requests);
- $responses = array_map(function ($conn) use ($readTimeout) {
- $response = new Response($conn['client']->wait_for_response_data($conn['requestId'], $readTimeout));
- if ($this->debug) {
- $response->debugOutput();
- }
- return $response;
- }, $connections);
- $this->message($successMessage);
- return $responses;
- } catch (\Exception $exception) {
- if ($errorMessage === null) {
- $this->error("Request failed", $exception);
- } else {
- $this->message($errorMessage);
- }
- return array_map(fn($request) => new Response(null, true), $requests);
- }
- }
- /**
- * Get client.
- *
- * @param string $address
- * @param bool $keepAlive
- *
- * @return Client
- */
- private function getClient(string $address = null, $keepAlive = false): Client
- {
- $address = $address ? $this->processTemplate($address) : $this->getAddr();
- if ($address[0] === '/') { // uds
- $host = 'unix://' . $address;
- $port = -1;
- } elseif ($address[0] === '[') { // ipv6
- $addressParts = explode(']:', $address);
- $host = $addressParts[0];
- if (isset($addressParts[1])) {
- $host .= ']';
- $port = $addressParts[1];
- } else {
- $port = $this->getPort();
- }
- } else { // ipv4
- $addressParts = explode(':', $address);
- $host = $addressParts[0];
- $port = $addressParts[1] ?? $this->getPort();
- }
- if ( ! $keepAlive) {
- return new Client($host, $port);
- }
- if ( ! isset($this->clients[$host][$port])) {
- $client = new Client($host, $port);
- $client->setKeepAlive(true);
- $this->clients[$host][$port] = $client;
- }
- return $this->clients[$host][$port];
- }
- /**
- * @return string
- */
- public function getUser()
- {
- return get_current_user();
- }
- /**
- * @return string
- */
- public function getGroup()
- {
- return get_current_group();
- }
- /**
- * @return int
- */
- public function getUid()
- {
- return getmyuid();
- }
- /**
- * @return int
- */
- public function getGid()
- {
- return getmygid();
- }
- /**
- * Reload FPM by sending USR2 signal and optionally change config before that.
- *
- * @param string|array $configTemplate
- *
- * @return string
- * @throws \Exception
- */
- public function reload($configTemplate = null)
- {
- if ( ! is_null($configTemplate)) {
- self::cleanConfigFiles();
- $this->configTemplate = $configTemplate;
- $this->createConfig();
- }
- return $this->signal('USR2');
- }
- /**
- * Reload FPM logs by sending USR1 signal.
- *
- * @return string
- * @throws \Exception
- */
- public function reloadLogs(): string
- {
- return $this->signal('USR1');
- }
- /**
- * Send signal to the supplied PID or the server PID.
- *
- * @param string $signal
- * @param int|null $pid
- *
- * @return string
- */
- public function signal($signal, int $pid = null)
- {
- if (is_null($pid)) {
- $pid = $this->getPid();
- }
- $cmd = "kill -$signal $pid";
- $this->trace('Sending signal using command', $cmd, true);
- return exec("kill -$signal $pid");
- }
- /**
- * Terminate master process
- */
- public function terminate()
- {
- proc_terminate($this->masterProcess);
- }
- /**
- * Close all open descriptors and process resources
- *
- * @param bool $terminate
- */
- public function close($terminate = false)
- {
- if ($terminate) {
- $this->terminate();
- }
- proc_close($this->masterProcess);
- }
- /**
- * Create a config file.
- *
- * @param string $extension
- *
- * @return string
- * @throws \Exception
- */
- private function createConfig($extension = 'ini')
- {
- if (is_array($this->configTemplate)) {
- $configTemplates = $this->configTemplate;
- if ( ! isset($configTemplates['main'])) {
- throw new \Exception('The config template array has to have main config');
- }
- $mainTemplate = $configTemplates['main'];
- if ( ! is_dir(self::CONF_DIR)) {
- mkdir(self::CONF_DIR);
- }
- foreach ($this->createPoolConfigs($configTemplates) as $name => $poolConfig) {
- $this->makeFile(
- 'conf',
- $this->processTemplate($poolConfig),
- self::CONF_DIR,
- $name
- );
- }
- } else {
- $mainTemplate = $this->configTemplate;
- }
- return $this->makeFile($extension, $this->processTemplate($mainTemplate));
- }
- /**
- * Create pool config templates.
- *
- * @param array $configTemplates
- *
- * @return array
- * @throws \Exception
- */
- private function createPoolConfigs(array $configTemplates)
- {
- if ( ! isset($configTemplates['poolTemplate'])) {
- unset($configTemplates['main']);
- return $configTemplates;
- }
- $poolTemplate = $configTemplates['poolTemplate'];
- $configs = [];
- if (isset($configTemplates['count'])) {
- $start = $configTemplates['start'] ?? 1;
- for ($i = $start; $i < $start + $configTemplates['count']; $i++) {
- $configs[$i] = str_replace('%index%', $i, $poolTemplate);
- }
- } elseif (isset($configTemplates['names'])) {
- foreach ($configTemplates['names'] as $name) {
- $configs[$name] = str_replace('%name%', $name, $poolTemplate);
- }
- } else {
- throw new \Exception('The config template requires count or names if poolTemplate set');
- }
- return $configs;
- }
- /**
- * Process template string.
- *
- * @param string $template
- *
- * @return string
- */
- private function processTemplate(string $template)
- {
- $vars = [
- 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC],
- 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR],
- 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW],
- 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID],
- 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC],
- 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR],
- 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW],
- 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID],
- 'ADDR:IPv4' => ['getAddr', 'ipv4'],
- 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'],
- 'ADDR:IPv6' => ['getAddr', 'ipv6'],
- 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'],
- 'ADDR:UDS' => ['getAddr', 'uds'],
- 'PORT' => ['getPort', 'ip'],
- 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf',
- 'USER' => ['getUser'],
- 'GROUP' => ['getGroup'],
- 'UID' => ['getUid'],
- 'GID' => ['getGid'],
- 'MASTER:OUT' => 'pipe:1',
- 'STDERR' => '/dev/stderr',
- 'STDOUT' => '/dev/stdout',
- ];
- $aliases = [
- 'ADDR' => 'ADDR:IPv4',
- 'FILE:LOG' => 'FILE:LOG:ERR',
- ];
- foreach ($aliases as $aliasName => $aliasValue) {
- $vars[$aliasName] = $vars[$aliasValue];
- }
- return preg_replace_callback(
- '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/',
- function ($matches) use ($vars) {
- $varName = $matches[1];
- if ( ! isset($vars[$varName])) {
- $this->error("Invalid config variable $varName");
- return 'INVALID';
- }
- $pool = $matches[2] ?? 'default';
- $varValue = $vars[$varName];
- if (is_string($varValue)) {
- return $varValue;
- }
- $functionName = array_shift($varValue);
- $varValue[] = $pool;
- return call_user_func_array([$this, $functionName], $varValue);
- },
- $template
- );
- }
- /**
- * @param string $type
- * @param string $pool
- *
- * @return string
- */
- public function getAddr(string $type = 'ipv4', $pool = 'default')
- {
- $port = $this->getPort($type, $pool, true);
- if ($type === 'uds') {
- $address = $this->getFile($port . '.sock');
- // Socket max path length is 108 on Linux and 104 on BSD,
- // so we use the latter
- if (strlen($address) <= 104) {
- return $address;
- }
- return sys_get_temp_dir() . '/' .
- hash('crc32', dirname($address)) . '-' .
- basename($address);
- }
- return $this->getHost($type) . ':' . $port;
- }
- /**
- * @param string $type
- * @param string $pool
- * @param bool $useAsId
- *
- * @return int
- */
- public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false)
- {
- if ($type === 'uds' && ! $useAsId) {
- return -1;
- }
- if (isset($this->ports['values'][$pool])) {
- return $this->ports['values'][$pool];
- }
- $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1;
- $this->ports['values'][$pool] = $this->ports['last'] = $port;
- return $port;
- }
- /**
- * @param string $type
- *
- * @return string
- */
- public function getHost(string $type = 'ipv4')
- {
- switch ($type) {
- case 'ipv6-any':
- return '[::]';
- case 'ipv6':
- return '[::1]';
- case 'ipv4-any':
- return '0.0.0.0';
- default:
- return '127.0.0.1';
- }
- }
- /**
- * Get listen address.
- *
- * @param string|null $template
- *
- * @return string
- */
- public function getListen($template = null)
- {
- return $template ? $this->processTemplate($template) : $this->getAddr();
- }
- /**
- * Get PID.
- *
- * @return int
- */
- public function getPid()
- {
- $pidFile = $this->getFile('pid');
- if ( ! is_file($pidFile)) {
- return (int)$this->error("PID file has not been created");
- }
- $pidContent = file_get_contents($pidFile);
- if ( ! is_numeric($pidContent)) {
- return (int)$this->error("PID content '$pidContent' is not integer");
- }
- $this->trace('PID found', $pidContent);
- return (int)$pidContent;
- }
- /**
- * Get file path for resource file.
- *
- * @param string $extension
- * @param string|null $dir
- * @param string|null $name
- *
- * @return string
- */
- private function getFile(string $extension, string $dir = null, string $name = null): string
- {
- $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension;
- return is_null($dir) ? $fileName : $dir . '/' . $fileName;
- }
- /**
- * Get absolute file path for the resource file used by templates.
- *
- * @param string $extension
- *
- * @return string
- */
- private function getAbsoluteFile(string $extension): string
- {
- return $this->getFile($extension);
- }
- /**
- * Get relative file name for resource file used by templates.
- *
- * @param string $extension
- *
- * @return string
- */
- private function getRelativeFile(string $extension): string
- {
- $fileName = rtrim(basename($this->fileName), '.');
- return $this->getFile($extension, null, $fileName);
- }
- /**
- * Get prefixed file.
- *
- * @param string $extension
- * @param string|null $prefix
- *
- * @return string
- */
- public function getPrefixedFile(string $extension, string $prefix = null): string
- {
- $fileName = rtrim($this->fileName, '.');
- if ( ! is_null($prefix)) {
- $fileName = $prefix . '/' . basename($fileName);
- }
- return $this->getFile($extension, null, $fileName);
- }
- /**
- * Create a resource file.
- *
- * @param string $extension
- * @param string $content
- * @param string|null $dir
- * @param string|null $name
- *
- * @return string
- */
- private function makeFile(
- string $extension,
- string $content = '',
- string $dir = null,
- string $name = null,
- bool $overwrite = true
- ): string {
- $filePath = $this->getFile($extension, $dir, $name);
- if ( ! $overwrite && is_file($filePath)) {
- return $filePath;
- }
- file_put_contents($filePath, $content);
- $this->trace('Created file: ' . $filePath, $content, isFile: true);
- return $filePath;
- }
- /**
- * Create a source code file.
- *
- * @return string
- */
- public function makeSourceFile(): string
- {
- return $this->makeFile('src.php', $this->code, overwrite: false);
- }
- /**
- * @param string|null $msg
- */
- private function message($msg)
- {
- if ($msg !== null) {
- echo "$msg\n";
- }
- }
- /**
- * Display error.
- *
- * @param string $msg
- * @param \Exception|null $exception
- *
- * @return false
- */
- private function error($msg, \Exception $exception = null): bool
- {
- $this->error = 'ERROR: ' . $msg;
- if ($exception) {
- $this->error .= '; EXCEPTION: ' . $exception->getMessage();
- }
- $this->error .= "\n";
- echo $this->error;
- return false;
- }
- /**
- * Check whether any error was set.
- *
- * @return bool
- */
- private function hasError()
- {
- return ! is_null($this->error) || ! is_null($this->logTool->getError());
- }
- /**
- * Expect file with a supplied extension to exist.
- *
- * @param string $extension
- * @param string $prefix
- *
- * @return bool
- */
- public function expectFile(string $extension, $prefix = null)
- {
- $filePath = $this->getPrefixedFile($extension, $prefix);
- if ( ! file_exists($filePath)) {
- return $this->error("The file $filePath does not exist");
- }
- $this->trace('File path exists as expected', $filePath);
- return true;
- }
- /**
- * Expect file with a supplied extension to not exist.
- *
- * @param string $extension
- * @param string $prefix
- *
- * @return bool
- */
- public function expectNoFile(string $extension, $prefix = null)
- {
- $filePath = $this->getPrefixedFile($extension, $prefix);
- if (file_exists($filePath)) {
- return $this->error("The file $filePath exists");
- }
- $this->trace('File path does not exist as expected', $filePath);
- return true;
- }
- /**
- * Expect message to be written to FastCGI error stream.
- *
- * @param string $message
- * @param int $limit
- * @param int $repeat
- */
- public function expectFastCGIErrorMessage(
- string $message,
- int $limit = 1024,
- int $repeat = 0
- ) {
- $this->logTool->setExpectedMessage($message, $limit, $repeat);
- $this->logTool->checkTruncatedMessage($this->response->getErrorData());
- }
- /**
- * Expect reloading lines to be logged.
- *
- * @param int $socketCount
- * @param bool $expectInitialProgressMessage
- * @param bool $expectReloadingMessage
- *
- * @throws \Exception
- */
- public function expectLogReloadingNotices(
- int $socketCount = 1,
- bool $expectInitialProgressMessage = true,
- bool $expectReloadingMessage = true
- ) {
- $this->logTool->expectReloadingLines(
- $socketCount,
- $expectInitialProgressMessage,
- $expectReloadingMessage
- );
- }
- /**
- * Expect reloading lines to be logged.
- *
- * @throws \Exception
- */
- public function expectLogReloadingLogsNotices()
- {
- $this->logTool->expectReloadingLogsLines();
- }
- /**
- * Expect starting lines to be logged.
- * @throws \Exception
- */
- public function expectLogStartNotices()
- {
- $this->logTool->expectStartingLines();
- }
- /**
- * Expect terminating lines to be logged.
- * @throws \Exception
- */
- public function expectLogTerminatingNotices()
- {
- $this->logTool->expectTerminatorLines();
- }
- /**
- * Expect log pattern in logs.
- *
- * @param string $pattern Log pattern
- *
- * @throws \Exception
- */
- public function expectLogPattern(string $pattern)
- {
- $this->logTool->expectPattern($pattern);
- }
- /**
- * Expect log message that can span multiple lines.
- *
- * @param string $message
- * @param int $limit
- * @param int $repeat
- * @param bool $decorated
- * @param bool $wrapped
- *
- * @throws \Exception
- */
- public function expectLogMessage(
- string $message,
- int $limit = 1024,
- int $repeat = 0,
- bool $decorated = true,
- bool $wrapped = true
- ) {
- $this->logTool->setExpectedMessage($message, $limit, $repeat);
- if ($wrapped) {
- $this->logTool->checkWrappedMessage(true, $decorated);
- } else {
- $this->logTool->checkTruncatedMessage();
- }
- }
- /**
- * Expect a single log line.
- *
- * @param string $message The expected message.
- * @param bool $isStdErr Whether it is logged to stderr.
- * @param bool $decorated Whether the log lines are decorated.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogLine(
- string $message,
- bool $isStdErr = true,
- bool $decorated = true
- ): bool {
- $messageLen = strlen($message);
- $limit = $messageLen > 1024 ? $messageLen + 16 : 1024;
- $this->logTool->setExpectedMessage($message, $limit);
- return $this->logTool->checkWrappedMessage(false, $decorated, $isStdErr);
- }
- /**
- * Expect log entry.
- *
- * @param string $type The log type
- * @param string $message The expected message
- * @param string|null $pool The pool for pool prefixed log entry
- * @param int $count The number of items
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- private function expectLogEntry(
- string $type,
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- for ($i = 0; $i < $count; $i++) {
- if ( ! $this->logTool->expectEntry($type, $message, $pool, $checkAllLogs)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Expect a log debug message.
- *
- * @param string $message
- * @param string|null $pool
- * @param int $count
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogDebug(
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- return $this->expectLogEntry(LogTool::DEBUG, $message, $pool, $count, $checkAllLogs);
- }
- /**
- * Expect a log notice.
- *
- * @param string $message
- * @param string|null $pool
- * @param int $count
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogNotice(
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- return $this->expectLogEntry(LogTool::NOTICE, $message, $pool, $count, $checkAllLogs);
- }
- /**
- * Expect a log warning.
- *
- * @param string $message
- * @param string|null $pool
- * @param int $count
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogWarning(
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- return $this->expectLogEntry(LogTool::WARNING, $message, $pool, $count, $checkAllLogs);
- }
- /**
- * Expect a log error.
- *
- * @param string $message
- * @param string|null $pool
- * @param int $count
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogError(
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- return $this->expectLogEntry(LogTool::ERROR, $message, $pool, $count, $checkAllLogs);
- }
- /**
- * Expect a log alert.
- *
- * @param string $message
- * @param string|null $pool
- * @param int $count
- * @param bool $checkAllLogs Whether to also check past logs.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogAlert(
- string $message,
- string $pool = null,
- int $count = 1,
- bool $checkAllLogs = false
- ): bool {
- return $this->expectLogEntry(LogTool::ALERT, $message, $pool, $count, $checkAllLogs);
- }
- /**
- * Expect no log lines to be logged.
- *
- * @return bool
- * @throws \Exception
- */
- public function expectNoLogMessages(): bool
- {
- $logLine = $this->logReader->getLine(timeoutSeconds: 0, timeoutMicroseconds: 1000);
- if ($logLine === "") {
- $logLine = $this->logReader->getLine(timeoutSeconds: 0, timeoutMicroseconds: 1000);
- }
- if ($logLine !== null) {
- return $this->error(
- "Expected no log lines but following line logged: $logLine"
- );
- }
- $this->trace('No log message received as expected');
- return true;
- }
- /**
- * Expect log config options
- *
- * @param array $options
- *
- * @return bool
- * @throws \Exception
- */
- public function expectLogConfigOptions(array $options)
- {
- foreach ($options as $name => $value) {
- $this->expectLogNotice("\s+$name\s=\s$value", checkAllLogs: true);
- }
- return true;
- }
- /**
- * Print content of access log.
- */
- public function printAccessLog()
- {
- $accessLog = $this->getFile('acc.log');
- if (is_file($accessLog)) {
- print file_get_contents($accessLog);
- }
- }
- /**
- * Read all log entries.
- *
- * @param string $type The log type
- * @param string $message The expected message
- * @param string|null $pool The pool for pool prefixed log entry
- *
- * @return bool
- * @throws \Exception
- */
- public function readAllLogEntries(string $type, string $message, string $pool = null): bool
- {
- return $this->logTool->readAllEntries($type, $message, $pool);
- }
- /**
- * Read all log entries.
- *
- * @param string $message The expected message
- * @param string|null $pool The pool for pool prefixed log entry
- *
- * @return bool
- * @throws \Exception
- */
- public function readAllLogNotices(string $message, string $pool = null): bool
- {
- return $this->readAllLogEntries(LogTool::NOTICE, $message, $pool);
- }
- /**
- * Switch the logs source.
- *
- * @param string $source The source file path or name if log is a pipe.
- *
- * @throws \Exception
- */
- public function switchLogSource(string $source)
- {
- $this->trace('Switching log descriptor to:', $source);
- $this->logReader->setFileSource($source, $this->processTemplate($source));
- }
- /**
- * Trace execution by printing supplied message only in debug mode.
- *
- * @param string $title Trace title to print if supplied.
- * @param string|array|null $message Message to print.
- * @param bool $isCommand Whether message is a command array.
- */
- private function trace(
- string $title,
- string|array $message = null,
- bool $isCommand = false,
- bool $isFile = false
- ): void {
- if ($this->debug) {
- echo "\n";
- echo ">>> $title\n";
- if (is_array($message)) {
- if ($isCommand) {
- echo implode(' ', $message) . "\n";
- } else {
- print_r($message);
- }
- } elseif ($message !== null) {
- if ($isFile) {
- $this->logReader->printSeparator();
- }
- echo $message . "\n";
- if ($isFile) {
- $this->logReader->printSeparator();
- }
- }
- }
- }
- }
|