tester.inc 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. <?php
  2. namespace FPM;
  3. use Adoy\FastCGI\Client;
  4. require_once 'fcgi.inc';
  5. require_once 'logtool.inc';
  6. require_once 'response.inc';
  7. class Tester
  8. {
  9. /**
  10. * Config directory for included files.
  11. */
  12. const CONF_DIR = __DIR__ . '/conf.d';
  13. /**
  14. * File extension for access log.
  15. */
  16. const FILE_EXT_LOG_ACC = 'acc.log';
  17. /**
  18. * File extension for error log.
  19. */
  20. const FILE_EXT_LOG_ERR = 'err.log';
  21. /**
  22. * File extension for slow log.
  23. */
  24. const FILE_EXT_LOG_SLOW = 'slow.log';
  25. /**
  26. * File extension for PID file.
  27. */
  28. const FILE_EXT_PID = 'pid';
  29. /**
  30. * @var array
  31. */
  32. static private $supportedFiles = [
  33. self::FILE_EXT_LOG_ACC,
  34. self::FILE_EXT_LOG_ERR,
  35. self::FILE_EXT_LOG_SLOW,
  36. self::FILE_EXT_PID,
  37. 'src.php',
  38. 'ini',
  39. 'skip.ini',
  40. '*.sock',
  41. ];
  42. /**
  43. * @var array
  44. */
  45. static private $filesToClean = ['.user.ini'];
  46. /**
  47. * @var bool
  48. */
  49. private $debug;
  50. /**
  51. * @var array
  52. */
  53. private $clients;
  54. /**
  55. * @var LogTool
  56. */
  57. private $logTool;
  58. /**
  59. * Configuration template
  60. *
  61. * @var string
  62. */
  63. private $configTemplate;
  64. /**
  65. * The PHP code to execute
  66. *
  67. * @var string
  68. */
  69. private $code;
  70. /**
  71. * @var array
  72. */
  73. private $options;
  74. /**
  75. * @var string
  76. */
  77. private $fileName;
  78. /**
  79. * @var resource
  80. */
  81. private $masterProcess;
  82. /**
  83. * @var resource
  84. */
  85. private $outDesc;
  86. /**
  87. * @var array
  88. */
  89. private $ports = [];
  90. /**
  91. * @var string
  92. */
  93. private $error;
  94. /**
  95. * The last response for the request call
  96. *
  97. * @var Response
  98. */
  99. private $response;
  100. /**
  101. * Clean all the created files up
  102. *
  103. * @param int $backTraceIndex
  104. */
  105. static public function clean($backTraceIndex = 1)
  106. {
  107. $filePrefix = self::getCallerFileName($backTraceIndex);
  108. if (substr($filePrefix, -6) === 'clean.') {
  109. $filePrefix = substr($filePrefix, 0, -6);
  110. }
  111. $filesToClean = array_merge(
  112. array_map(
  113. function($fileExtension) use ($filePrefix) {
  114. return $filePrefix . $fileExtension;
  115. },
  116. self::$supportedFiles
  117. ),
  118. array_map(
  119. function($fileExtension) {
  120. return __DIR__ . '/' . $fileExtension;
  121. },
  122. self::$filesToClean
  123. )
  124. );
  125. // clean all the root files
  126. foreach ($filesToClean as $filePattern) {
  127. foreach (glob($filePattern) as $filePath) {
  128. unlink($filePath);
  129. }
  130. }
  131. // clean config files
  132. if (is_dir(self::CONF_DIR)) {
  133. foreach(glob(self::CONF_DIR . '/*.conf') as $name) {
  134. unlink($name);
  135. }
  136. rmdir(self::CONF_DIR);
  137. }
  138. }
  139. /**
  140. * @param int $backTraceIndex
  141. * @return string
  142. */
  143. static private function getCallerFileName($backTraceIndex = 1)
  144. {
  145. $backtrace = debug_backtrace();
  146. if (isset($backtrace[$backTraceIndex]['file'])) {
  147. $filePath = $backtrace[$backTraceIndex]['file'];
  148. } else {
  149. $filePath = __FILE__;
  150. }
  151. return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION)));
  152. }
  153. /**
  154. * @return bool|string
  155. */
  156. static public function findExecutable()
  157. {
  158. $phpPath = getenv("TEST_PHP_EXECUTABLE");
  159. for ($i = 0; $i < 2; $i++) {
  160. $slashPosition = strrpos($phpPath, "/");
  161. if ($slashPosition) {
  162. $phpPath = substr($phpPath, 0, $slashPosition);
  163. } else {
  164. break;
  165. }
  166. }
  167. if ($phpPath && is_dir($phpPath)) {
  168. if (file_exists($phpPath."/fpm/php-fpm") && is_executable($phpPath."/fpm/php-fpm")) {
  169. /* gotcha */
  170. return $phpPath."/fpm/php-fpm";
  171. }
  172. $phpSbinFpmi = $phpPath."/sbin/php-fpm";
  173. if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) {
  174. return $phpSbinFpmi;
  175. }
  176. }
  177. // try local php-fpm
  178. $fpmPath = dirname(__DIR__) . '/php-fpm';
  179. if (file_exists($fpmPath) && is_executable($fpmPath)) {
  180. return $fpmPath;
  181. }
  182. return false;
  183. }
  184. /**
  185. * Skip test if any of the supplied files does not exist.
  186. *
  187. * @param mixed $files
  188. */
  189. static public function skipIfAnyFileDoesNotExist($files)
  190. {
  191. if (!is_array($files)) {
  192. $files = array($files);
  193. }
  194. foreach ($files as $file) {
  195. if (!file_exists($file)) {
  196. die("skip File $file does not exist");
  197. }
  198. }
  199. }
  200. /**
  201. * Skip test if config file is invalid.
  202. *
  203. * @param string $configTemplate
  204. * @throws \Exception
  205. */
  206. static public function skipIfConfigFails(string $configTemplate)
  207. {
  208. $tester = new self($configTemplate, '', [], self::getCallerFileName());
  209. $testResult = $tester->testConfig();
  210. if ($testResult !== null) {
  211. self::clean(2);
  212. die("skip $testResult");
  213. }
  214. }
  215. /**
  216. * Skip test if IPv6 is not supported.
  217. */
  218. static public function skipIfIPv6IsNotSupported()
  219. {
  220. @stream_socket_client('tcp://[::1]:0', $errno);
  221. if ($errno != 111) {
  222. die('skip IPv6 is not supported.');
  223. }
  224. }
  225. /**
  226. * Skip if running on Travis.
  227. *
  228. * @param $message
  229. */
  230. static public function skipIfTravis($message)
  231. {
  232. if (getenv("TRAVIS")) {
  233. die('skip Travis: ' . $message);
  234. }
  235. }
  236. /**
  237. * Tester constructor.
  238. *
  239. * @param string|array $configTemplate
  240. * @param string $code
  241. * @param array $options
  242. * @param string $fileName
  243. */
  244. public function __construct(
  245. $configTemplate,
  246. string $code = '',
  247. array $options = [],
  248. $fileName = null
  249. ) {
  250. $this->configTemplate = $configTemplate;
  251. $this->code = $code;
  252. $this->options = $options;
  253. $this->fileName = $fileName ?: self::getCallerFileName();
  254. $this->logTool = new LogTool();
  255. $this->debug = (bool) getenv('TEST_FPM_DEBUG');
  256. }
  257. /**
  258. * @param string $ini
  259. */
  260. public function setUserIni(string $ini)
  261. {
  262. $iniFile = __DIR__ . '/.user.ini';
  263. file_put_contents($iniFile, $ini);
  264. }
  265. /**
  266. * Test configuration file.
  267. *
  268. * @return null|string
  269. * @throws \Exception
  270. */
  271. public function testConfig()
  272. {
  273. $configFile = $this->createConfig();
  274. $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1';
  275. exec($cmd, $output, $code);
  276. if ($code) {
  277. return preg_replace("/\[.+?\]/", "", $output[0]);
  278. }
  279. return null;
  280. }
  281. /**
  282. * Start PHP-FPM master process
  283. *
  284. * @param string $extraArgs
  285. * @return bool
  286. * @throws \Exception
  287. */
  288. public function start(string $extraArgs = '')
  289. {
  290. $configFile = $this->createConfig();
  291. $desc = $this->outDesc ? [] : [1 => array('pipe', 'w')];
  292. $asRoot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : '';
  293. $cmd = self::findExecutable() . " $asRoot -F -O -y $configFile $extraArgs";
  294. /* Since it's not possible to spawn a process under linux without using a
  295. * shell in php (why?!?) we need a little shell trickery, so that we can
  296. * actually kill php-fpm */
  297. $this->masterProcess = proc_open(
  298. "killit () { kill \$child 2> /dev/null; }; " .
  299. "trap killit TERM; $cmd 2>&1 & child=\$!; wait",
  300. $desc,
  301. $pipes
  302. );
  303. register_shutdown_function(
  304. function($masterProcess) use($configFile) {
  305. @unlink($configFile);
  306. if (is_resource($masterProcess)) {
  307. @proc_terminate($masterProcess);
  308. while (proc_get_status($masterProcess)['running']) {
  309. usleep(10000);
  310. }
  311. }
  312. },
  313. $this->masterProcess
  314. );
  315. if (!$this->outDesc !== false) {
  316. $this->outDesc = $pipes[1];
  317. }
  318. return true;
  319. }
  320. /**
  321. * Run until needle is found in the log.
  322. *
  323. * @param string $needle
  324. * @param int $max
  325. * @return bool
  326. * @throws \Exception
  327. */
  328. public function runTill(string $needle, $max = 10)
  329. {
  330. $this->start();
  331. $found = false;
  332. for ($i = 0; $i < $max; $i++) {
  333. $line = $this->getLogLine();
  334. if (is_null($line)) {
  335. break;
  336. }
  337. if (preg_match($needle, $line) === 1) {
  338. $found = true;
  339. break;
  340. }
  341. }
  342. $this->close(true);
  343. if (!$found) {
  344. return $this->error("The search pattern not found");
  345. }
  346. return true;
  347. }
  348. /**
  349. * Check if connection works.
  350. *
  351. * @param string $host
  352. * @param null|string $successMessage
  353. * @param null|string $errorMessage
  354. * @param int $attempts
  355. * @param int $delay
  356. */
  357. public function checkConnection(
  358. $host = '127.0.0.1',
  359. $successMessage = null,
  360. $errorMessage = 'Connection failed',
  361. $attempts = 20,
  362. $delay = 50000
  363. ) {
  364. $i = 0;
  365. do {
  366. if ($i > 0 && $delay > 0) {
  367. usleep($delay);
  368. }
  369. $fp = @fsockopen($host, $this->getPort());
  370. } while ((++$i < $attempts) && !$fp);
  371. if ($fp) {
  372. $this->message($successMessage);
  373. fclose($fp);
  374. } else {
  375. $this->message($errorMessage);
  376. }
  377. }
  378. /**
  379. * Execute request with parameters ordered for better checking.
  380. *
  381. * @param string $address
  382. * @param string|null $successMessage
  383. * @param string|null $errorMessage
  384. * @param string $uri
  385. * @param string $query
  386. * @param array $headers
  387. * @return Response
  388. */
  389. public function checkRequest(
  390. string $address,
  391. string $successMessage = null,
  392. string $errorMessage = null,
  393. $uri = '/ping',
  394. $query = '',
  395. $headers = []
  396. ) {
  397. return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage);
  398. }
  399. /**
  400. * Execute and check ping request.
  401. *
  402. * @param string $address
  403. * @param string $pingPath
  404. * @param string $pingResponse
  405. */
  406. public function ping(
  407. string $address = '{{ADDR}}',
  408. string $pingResponse = 'pong',
  409. string $pingPath = '/ping'
  410. ) {
  411. $response = $this->request('', [], $pingPath, $address);
  412. $response->expectBody($pingResponse, 'text/plain');
  413. }
  414. /**
  415. * Execute and check status request(s).
  416. *
  417. * @param array $expectedFields
  418. * @param string|null $address
  419. * @param string $statusPath
  420. * @param mixed $formats
  421. * @throws \Exception
  422. */
  423. public function status(
  424. array $expectedFields,
  425. string $address = null,
  426. string $statusPath = '/status',
  427. $formats = ['plain', 'html', 'xml', 'json']
  428. ) {
  429. if (!is_array($formats)) {
  430. $formats = [$formats];
  431. }
  432. require_once "status.inc";
  433. $status = new Status();
  434. foreach ($formats as $format) {
  435. $query = $format === 'plain' ? '' : $format;
  436. $response = $this->request($query, [], $statusPath, $address);
  437. $status->checkStatus($response, $expectedFields, $format);
  438. }
  439. }
  440. /**
  441. * Execute request.
  442. *
  443. * @param string $query
  444. * @param array $headers
  445. * @param string|null $uri
  446. * @param string|null $address
  447. * @param string|null $successMessage
  448. * @param string|null $errorMessage
  449. * @param bool $connKeepAlive
  450. * @return Response
  451. */
  452. public function request(
  453. string $query = '',
  454. array $headers = [],
  455. string $uri = null,
  456. string $address = null,
  457. string $successMessage = null,
  458. string $errorMessage = null,
  459. bool $connKeepAlive = false
  460. ) {
  461. if ($this->hasError()) {
  462. return new Response(null, true);
  463. }
  464. if (is_null($uri)) {
  465. $uri = $this->makeSourceFile();
  466. }
  467. $params = array_merge(
  468. [
  469. 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
  470. 'REQUEST_METHOD' => 'GET',
  471. 'SCRIPT_FILENAME' => $uri,
  472. 'SCRIPT_NAME' => $uri,
  473. 'QUERY_STRING' => $query,
  474. 'REQUEST_URI' => $uri . ($query ? '?'.$query : ""),
  475. 'DOCUMENT_URI' => $uri,
  476. 'SERVER_SOFTWARE' => 'php/fcgiclient',
  477. 'REMOTE_ADDR' => '127.0.0.1',
  478. 'REMOTE_PORT' => '7777',
  479. 'SERVER_ADDR' => '127.0.0.1',
  480. 'SERVER_PORT' => '80',
  481. 'SERVER_NAME' => php_uname('n'),
  482. 'SERVER_PROTOCOL' => 'HTTP/1.1',
  483. 'DOCUMENT_ROOT' => __DIR__,
  484. 'CONTENT_TYPE' => '',
  485. 'CONTENT_LENGTH' => 0
  486. ],
  487. $headers
  488. );
  489. try {
  490. $this->response = new Response(
  491. $this->getClient($address, $connKeepAlive)->request_data($params, false)
  492. );
  493. $this->message($successMessage);
  494. } catch (\Exception $exception) {
  495. if ($errorMessage === null) {
  496. $this->error("Request failed", $exception);
  497. } else {
  498. $this->message($errorMessage);
  499. }
  500. $this->response = new Response();
  501. }
  502. if ($this->debug) {
  503. $this->response->debugOutput();
  504. }
  505. return $this->response;
  506. }
  507. /**
  508. * Get client.
  509. *
  510. * @param string $address
  511. * @param bool $keepAlive
  512. * @return Client
  513. */
  514. private function getClient(string $address = null, $keepAlive = false)
  515. {
  516. $address = $address ? $this->processTemplate($address) : $this->getAddr();
  517. if ($address[0] === '/') { // uds
  518. $host = 'unix://' . $address;
  519. $port = -1;
  520. } elseif ($address[0] === '[') { // ipv6
  521. $addressParts = explode(']:', $address);
  522. $host = $addressParts[0];
  523. if (isset($addressParts[1])) {
  524. $host .= ']';
  525. $port = $addressParts[1];
  526. } else {
  527. $port = $this->getPort();
  528. }
  529. } else { // ipv4
  530. $addressParts = explode(':', $address);
  531. $host = $addressParts[0];
  532. $port = $addressParts[1] ?? $this->getPort();
  533. }
  534. if (!$keepAlive) {
  535. return new Client($host, $port);
  536. }
  537. if (!isset($this->clients[$host][$port])) {
  538. $client = new Client($host, $port);
  539. $client->setKeepAlive(true);
  540. $this->clients[$host][$port] = $client;
  541. }
  542. return $this->clients[$host][$port];
  543. }
  544. /**
  545. * Display logs
  546. *
  547. * @param int $number
  548. * @param string $ignore
  549. */
  550. public function displayLog(int $number = 1, string $ignore = 'systemd')
  551. {
  552. /* Read $number lines or until EOF */
  553. while ($number > 0 || ($number < 0 && !feof($this->outDesc))) {
  554. $a = fgets($this->outDesc);
  555. if (empty($ignore) || !strpos($a, $ignore)) {
  556. echo $a;
  557. $number--;
  558. }
  559. }
  560. }
  561. /**
  562. * Get a single log line
  563. *
  564. * @return null|string
  565. */
  566. private function getLogLine()
  567. {
  568. $read = [$this->outDesc];
  569. $write = null;
  570. $except = null;
  571. if (stream_select($read, $write, $except, 2 )) {
  572. return fgets($this->outDesc);
  573. } else {
  574. return null;
  575. }
  576. }
  577. /**
  578. * Get log lines
  579. *
  580. * @param int $number
  581. * @param bool $skipBlank
  582. * @param string $ignore
  583. * @return array
  584. */
  585. public function getLogLines(int $number = 1, bool $skipBlank = false, string $ignore = 'systemd')
  586. {
  587. $lines = [];
  588. /* Read $n lines or until EOF */
  589. while ($number > 0 || ($number < 0 && !feof($this->outDesc))) {
  590. $line = $this->getLogLine();
  591. if (is_null($line)) {
  592. break;
  593. }
  594. if ((empty($ignore) || !strpos($line, $ignore)) && (!$skipBlank || strlen(trim($line)) > 0)) {
  595. $lines[] = $line;
  596. $number--;
  597. }
  598. }
  599. return $lines;
  600. }
  601. /**
  602. * @return mixed|string
  603. */
  604. public function getLastLogLine()
  605. {
  606. $lines = $this->getLogLines();
  607. return $lines[0] ?? '';
  608. }
  609. /**
  610. * Send signal to the supplied PID or the server PID.
  611. *
  612. * @param string $signal
  613. * @param int|null $pid
  614. * @return string
  615. */
  616. public function signal($signal, int $pid = null)
  617. {
  618. if (is_null($pid)) {
  619. $pid = $this->getPid();
  620. }
  621. return exec("kill -$signal $pid");
  622. }
  623. /**
  624. * Terminate master process
  625. */
  626. public function terminate()
  627. {
  628. proc_terminate($this->masterProcess);
  629. }
  630. /**
  631. * Close all open descriptors and process resources
  632. *
  633. * @param bool $terminate
  634. */
  635. public function close($terminate = false)
  636. {
  637. if ($terminate) {
  638. $this->terminate();
  639. }
  640. fclose($this->outDesc);
  641. proc_close($this->masterProcess);
  642. }
  643. /**
  644. * Create a config file.
  645. *
  646. * @param string $extension
  647. * @return string
  648. * @throws \Exception
  649. */
  650. private function createConfig($extension = 'ini')
  651. {
  652. if (is_array($this->configTemplate)) {
  653. $configTemplates = $this->configTemplate;
  654. if (!isset($configTemplates['main'])) {
  655. throw new \Exception('The config template array has to have main config');
  656. }
  657. $mainTemplate = $configTemplates['main'];
  658. unset($configTemplates['main']);
  659. if (!is_dir(self::CONF_DIR)) {
  660. mkdir(self::CONF_DIR);
  661. }
  662. foreach ($configTemplates as $name => $configTemplate) {
  663. $this->makeFile(
  664. 'conf',
  665. $this->processTemplate($configTemplate),
  666. self::CONF_DIR,
  667. $name
  668. );
  669. }
  670. } else {
  671. $mainTemplate = $this->configTemplate;
  672. }
  673. return $this->makeFile($extension, $this->processTemplate($mainTemplate));
  674. }
  675. /**
  676. * Process template string.
  677. *
  678. * @param string $template
  679. * @return string
  680. */
  681. private function processTemplate(string $template)
  682. {
  683. $vars = [
  684. 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC],
  685. 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR],
  686. 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW],
  687. 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID],
  688. 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC],
  689. 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR],
  690. 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW],
  691. 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID],
  692. 'ADDR:IPv4' => ['getAddr', 'ipv4'],
  693. 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'],
  694. 'ADDR:IPv6' => ['getAddr', 'ipv6'],
  695. 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'],
  696. 'ADDR:UDS' => ['getAddr', 'uds'],
  697. 'PORT' => ['getPort', 'ip'],
  698. 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf',
  699. ];
  700. $aliases = [
  701. 'ADDR' => 'ADDR:IPv4',
  702. 'FILE:LOG' => 'FILE:LOG:ERR',
  703. ];
  704. foreach ($aliases as $aliasName => $aliasValue) {
  705. $vars[$aliasName] = $vars[$aliasValue];
  706. }
  707. return preg_replace_callback(
  708. '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/',
  709. function ($matches) use ($vars) {
  710. $varName = $matches[1];
  711. if (!isset($vars[$varName])) {
  712. $this->error("Invalid config variable $varName");
  713. return 'INVALID';
  714. }
  715. $pool = $matches[2] ?? 'default';
  716. $varValue = $vars[$varName];
  717. if (is_string($varValue)) {
  718. return $varValue;
  719. }
  720. $functionName = array_shift($varValue);
  721. $varValue[] = $pool;
  722. return call_user_func_array([$this, $functionName], $varValue);
  723. },
  724. $template
  725. );
  726. }
  727. /**
  728. * @param string $type
  729. * @param string $pool
  730. * @return string
  731. */
  732. public function getAddr(string $type = 'ipv4', $pool = 'default')
  733. {
  734. $port = $this->getPort($type, $pool, true);
  735. if ($type === 'uds') {
  736. return $this->getFile($port . '.sock');
  737. }
  738. return $this->getHost($type) . ':' . $port;
  739. }
  740. /**
  741. * @param string $type
  742. * @param string $pool
  743. * @param bool $useAsId
  744. * @return int
  745. */
  746. public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false)
  747. {
  748. if ($type === 'uds' && !$useAsId) {
  749. return -1;
  750. }
  751. if (isset($this->ports['values'][$pool])) {
  752. return $this->ports['values'][$pool];
  753. }
  754. $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1;
  755. $this->ports['values'][$pool] = $this->ports['last'] = $port;
  756. return $port;
  757. }
  758. /**
  759. * @param string $type
  760. * @return string
  761. */
  762. public function getHost(string $type = 'ipv4')
  763. {
  764. switch ($type) {
  765. case 'ipv6-any':
  766. return '[::]';
  767. case 'ipv6':
  768. return '[::1]';
  769. case 'ipv4-any':
  770. return '0.0.0.0';
  771. default:
  772. return '127.0.0.1';
  773. }
  774. }
  775. /**
  776. * Get listen address.
  777. *
  778. * @param string|null $template
  779. * @return string
  780. */
  781. public function getListen($template = null)
  782. {
  783. return $template ? $this->processTemplate($template) : $this->getAddr();
  784. }
  785. /**
  786. * Get PID.
  787. *
  788. * @return int
  789. */
  790. public function getPid()
  791. {
  792. $pidFile = $this->getFile('pid');
  793. if (!is_file($pidFile)) {
  794. return (int) $this->error("PID file has not been created");
  795. }
  796. $pidContent = file_get_contents($pidFile);
  797. if (!is_numeric($pidContent)) {
  798. return (int) $this->error("PID content '$pidContent' is not integer");
  799. }
  800. return (int) $pidContent;
  801. }
  802. /**
  803. * @param string $extension
  804. * @param string|null $dir
  805. * @param string|null $name
  806. * @return string
  807. */
  808. private function getFile(string $extension, $dir = null, $name = null)
  809. {
  810. $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension;
  811. return is_null($dir) ? $fileName : $dir . '/' . $fileName;
  812. }
  813. /**
  814. * @param string $extension
  815. * @return string
  816. */
  817. private function getAbsoluteFile(string $extension)
  818. {
  819. return $this->getFile($extension);
  820. }
  821. /**
  822. * @param string $extension
  823. * @return string
  824. */
  825. private function getRelativeFile(string $extension)
  826. {
  827. $fileName = rtrim(basename($this->fileName), '.');
  828. return $this->getFile($extension, null, $fileName);
  829. }
  830. /**
  831. * @param string $extension
  832. * @param string $prefix
  833. * @return string
  834. */
  835. private function getPrefixedFile(string $extension, string $prefix = null)
  836. {
  837. $fileName = rtrim($this->fileName, '.');
  838. if (!is_null($prefix)) {
  839. $fileName = $prefix . '/' . basename($fileName);
  840. }
  841. return $this->getFile($extension, null, $fileName);
  842. }
  843. /**
  844. * @param string $extension
  845. * @param string $content
  846. * @param string|null $dir
  847. * @param string|null $name
  848. * @return string
  849. */
  850. private function makeFile(string $extension, string $content = '', $dir = null, $name = null)
  851. {
  852. $filePath = $this->getFile($extension, $dir, $name);
  853. file_put_contents($filePath, $content);
  854. return $filePath;
  855. }
  856. /**
  857. * @return string
  858. */
  859. public function makeSourceFile()
  860. {
  861. return $this->makeFile('src.php', $this->code);
  862. }
  863. /**
  864. * @param string|null $msg
  865. */
  866. private function message($msg)
  867. {
  868. if ($msg !== null) {
  869. echo "$msg\n";
  870. }
  871. }
  872. /**
  873. * @param string $msg
  874. * @param \Exception|null $exception
  875. */
  876. private function error($msg, \Exception $exception = null)
  877. {
  878. $this->error = 'ERROR: ' . $msg;
  879. if ($exception) {
  880. $this->error .= '; EXCEPTION: ' . $exception->getMessage();
  881. }
  882. $this->error .= "\n";
  883. echo $this->error;
  884. }
  885. /**
  886. * @return bool
  887. */
  888. private function hasError()
  889. {
  890. return !is_null($this->error) || !is_null($this->logTool->getError());
  891. }
  892. /**
  893. * Expect file with a supplied extension to exist.
  894. *
  895. * @param string $extension
  896. * @param string $prefix
  897. * @return bool
  898. */
  899. public function expectFile(string $extension, $prefix = null)
  900. {
  901. $filePath = $this->getPrefixedFile($extension, $prefix);
  902. if (!file_exists($filePath)) {
  903. return $this->error("The file $filePath does not exist");
  904. }
  905. return true;
  906. }
  907. /**
  908. * Expect file with a supplied extension to not exist.
  909. *
  910. * @param string $extension
  911. * @param string $prefix
  912. * @return bool
  913. */
  914. public function expectNoFile(string $extension, $prefix = null)
  915. {
  916. $filePath = $this->getPrefixedFile($extension, $prefix);
  917. if (file_exists($filePath)) {
  918. return $this->error("The file $filePath exists");
  919. }
  920. return true;
  921. }
  922. /**
  923. * Expect message to be written to FastCGI error stream.
  924. *
  925. * @param string $message
  926. * @param int $limit
  927. * @param int $repeat
  928. */
  929. public function expectFastCGIErrorMessage(
  930. string $message,
  931. int $limit = 1024,
  932. int $repeat = 0
  933. ) {
  934. $this->logTool->setExpectedMessage($message, $limit, $repeat);
  935. $this->logTool->checkTruncatedMessage($this->response->getErrorData());
  936. }
  937. /**
  938. * Expect starting lines to be logged.
  939. */
  940. public function expectLogStartNotices()
  941. {
  942. $this->logTool->expectStartingLines($this->getLogLines(2));
  943. }
  944. /**
  945. * Expect terminating lines to be logged.
  946. */
  947. public function expectLogTerminatingNotices()
  948. {
  949. $this->logTool->expectTerminatorLines($this->getLogLines(-1));
  950. }
  951. /**
  952. * Expect log message that can span multiple lines.
  953. *
  954. * @param string $message
  955. * @param int $limit
  956. * @param int $repeat
  957. * @param bool $decorated
  958. * @param bool $wrapped
  959. */
  960. public function expectLogMessage(
  961. string $message,
  962. int $limit = 1024,
  963. int $repeat = 0,
  964. bool $decorated = true,
  965. bool $wrapped = true
  966. ) {
  967. $this->logTool->setExpectedMessage($message, $limit, $repeat);
  968. if ($wrapped) {
  969. $logLines = $this->getLogLines(-1, true);
  970. $this->logTool->checkWrappedMessage($logLines, true, $decorated);
  971. } else {
  972. $logLines = $this->getLogLines(1, true);
  973. $this->logTool->checkTruncatedMessage($logLines[0] ?? '');
  974. }
  975. if ($this->debug) {
  976. $this->message("-------------- LOG LINES: -------------");
  977. var_dump($logLines);
  978. $this->message("---------------------------------------\n");
  979. }
  980. }
  981. /**
  982. * Expect a single log line.
  983. *
  984. * @param string $message
  985. * @return bool
  986. */
  987. public function expectLogLine(string $message, bool $is_stderr = true)
  988. {
  989. $messageLen = strlen($message);
  990. $limit = $messageLen > 1024 ? $messageLen + 16 : 1024;
  991. $this->logTool->setExpectedMessage($message, $limit);
  992. $logLines = $this->getLogLines(1, true);
  993. if ($this->debug) {
  994. $this->message("LOG LINE: " . ($logLines[0] ?? ''));
  995. }
  996. return $this->logTool->checkWrappedMessage($logLines, false, true, $is_stderr);
  997. }
  998. /**
  999. * Expect a log debug message.
  1000. *
  1001. * @param string $message
  1002. * @param string|null $pool
  1003. * @return bool
  1004. */
  1005. public function expectLogDebug(string $message, $pool = null)
  1006. {
  1007. return $this->logTool->expectDebug($this->getLastLogLine(), $message, $pool);
  1008. }
  1009. /**
  1010. * Expect a log notice.
  1011. *
  1012. * @param string $message
  1013. * @param string|null $pool
  1014. * @return bool
  1015. */
  1016. public function expectLogNotice(string $message, $pool = null)
  1017. {
  1018. return $this->logTool->expectNotice($this->getLastLogLine(), $message, $pool);
  1019. }
  1020. /**
  1021. * Expect a log warning.
  1022. *
  1023. * @param string $message
  1024. * @param string|null $pool
  1025. * @return bool
  1026. */
  1027. public function expectLogWarning(string $message, $pool = null)
  1028. {
  1029. return $this->logTool->expectWarning($this->getLastLogLine(), $message, $pool);
  1030. }
  1031. /**
  1032. * Expect a log error.
  1033. *
  1034. * @param string $message
  1035. * @param string|null $pool
  1036. * @return bool
  1037. */
  1038. public function expectLogError(string $message, $pool = null)
  1039. {
  1040. return $this->logTool->expectError($this->getLastLogLine(), $message, $pool);
  1041. }
  1042. /**
  1043. * Expect a log alert.
  1044. *
  1045. * @param string $message
  1046. * @param string|null $pool
  1047. * @return bool
  1048. */
  1049. public function expectLogAlert(string $message, $pool = null)
  1050. {
  1051. return $this->logTool->expectAlert($this->getLastLogLine(), $message, $pool);
  1052. }
  1053. /**
  1054. * Expect no log lines to be logged.
  1055. *
  1056. * @return bool
  1057. */
  1058. public function expectNoLogMessages()
  1059. {
  1060. $logLines = $this->getLogLines(-1, true);
  1061. if (!empty($logLines)) {
  1062. return $this->error(
  1063. "Expected no log lines but following lines logged:\n" . implode("\n", $logLines)
  1064. );
  1065. }
  1066. return true;
  1067. }
  1068. /**
  1069. * Print content of access log.
  1070. */
  1071. public function printAccessLog()
  1072. {
  1073. $accessLog = $this->getFile('acc.log');
  1074. if (is_file($accessLog)) {
  1075. print file_get_contents($accessLog);
  1076. }
  1077. }
  1078. }