tester.inc 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641
  1. <?php
  2. namespace FPM;
  3. use Adoy\FastCGI\Client;
  4. require_once 'fcgi.inc';
  5. require_once 'logreader.inc';
  6. require_once 'logtool.inc';
  7. require_once 'response.inc';
  8. class Tester
  9. {
  10. /**
  11. * Config directory for included files.
  12. */
  13. const CONF_DIR = __DIR__ . '/conf.d';
  14. /**
  15. * File extension for access log.
  16. */
  17. const FILE_EXT_LOG_ACC = 'acc.log';
  18. /**
  19. * File extension for error log.
  20. */
  21. const FILE_EXT_LOG_ERR = 'err.log';
  22. /**
  23. * File extension for slow log.
  24. */
  25. const FILE_EXT_LOG_SLOW = 'slow.log';
  26. /**
  27. * File extension for PID file.
  28. */
  29. const FILE_EXT_PID = 'pid';
  30. /**
  31. * @var array
  32. */
  33. static private array $supportedFiles = [
  34. self::FILE_EXT_LOG_ACC,
  35. self::FILE_EXT_LOG_ERR,
  36. self::FILE_EXT_LOG_SLOW,
  37. self::FILE_EXT_PID,
  38. 'src.php',
  39. 'ini',
  40. 'skip.ini',
  41. '*.sock',
  42. ];
  43. /**
  44. * @var array
  45. */
  46. static private array $filesToClean = ['.user.ini'];
  47. /**
  48. * @var bool
  49. */
  50. private bool $debug;
  51. /**
  52. * @var array
  53. */
  54. private array $clients = [];
  55. /**
  56. * @var LogReader
  57. */
  58. private LogReader $logReader;
  59. /**
  60. * @var LogTool
  61. */
  62. private LogTool $logTool;
  63. /**
  64. * Configuration template
  65. *
  66. * @var string|array
  67. */
  68. private string|array $configTemplate;
  69. /**
  70. * The PHP code to execute
  71. *
  72. * @var string
  73. */
  74. private string $code;
  75. /**
  76. * @var array
  77. */
  78. private array $options;
  79. /**
  80. * @var string
  81. */
  82. private string $fileName;
  83. /**
  84. * @var resource
  85. */
  86. private $masterProcess;
  87. /**
  88. * @var resource
  89. */
  90. private $outDesc;
  91. /**
  92. * @var array
  93. */
  94. private array $ports = [];
  95. /**
  96. * @var string|null
  97. */
  98. private ?string $error = null;
  99. /**
  100. * The last response for the request call
  101. *
  102. * @var Response|null
  103. */
  104. private ?Response $response;
  105. /**
  106. * Clean all the created files up
  107. *
  108. * @param int $backTraceIndex
  109. */
  110. static public function clean($backTraceIndex = 1)
  111. {
  112. $filePrefix = self::getCallerFileName($backTraceIndex);
  113. if (str_ends_with($filePrefix, 'clean.')) {
  114. $filePrefix = substr($filePrefix, 0, -6);
  115. }
  116. $filesToClean = array_merge(
  117. array_map(
  118. function ($fileExtension) use ($filePrefix) {
  119. return $filePrefix . $fileExtension;
  120. },
  121. self::$supportedFiles
  122. ),
  123. array_map(
  124. function ($fileExtension) {
  125. return __DIR__ . '/' . $fileExtension;
  126. },
  127. self::$filesToClean
  128. )
  129. );
  130. // clean all the root files
  131. foreach ($filesToClean as $filePattern) {
  132. foreach (glob($filePattern) as $filePath) {
  133. unlink($filePath);
  134. }
  135. }
  136. self::cleanConfigFiles();
  137. }
  138. /**
  139. * Clean config files
  140. */
  141. static public function cleanConfigFiles()
  142. {
  143. if (is_dir(self::CONF_DIR)) {
  144. foreach (glob(self::CONF_DIR . '/*.conf') as $name) {
  145. unlink($name);
  146. }
  147. rmdir(self::CONF_DIR);
  148. }
  149. }
  150. /**
  151. * @param int $backTraceIndex
  152. *
  153. * @return string
  154. */
  155. static private function getCallerFileName(int $backTraceIndex = 1): string
  156. {
  157. $backtrace = debug_backtrace();
  158. if (isset($backtrace[$backTraceIndex]['file'])) {
  159. $filePath = $backtrace[$backTraceIndex]['file'];
  160. } else {
  161. $filePath = __FILE__;
  162. }
  163. return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION)));
  164. }
  165. /**
  166. * @return bool|string
  167. */
  168. static public function findExecutable(): bool|string
  169. {
  170. $phpPath = getenv("TEST_PHP_EXECUTABLE");
  171. for ($i = 0; $i < 2; $i++) {
  172. $slashPosition = strrpos($phpPath, "/");
  173. if ($slashPosition) {
  174. $phpPath = substr($phpPath, 0, $slashPosition);
  175. } else {
  176. break;
  177. }
  178. }
  179. if ($phpPath && is_dir($phpPath)) {
  180. if (file_exists($phpPath . "/fpm/php-fpm") && is_executable($phpPath . "/fpm/php-fpm")) {
  181. /* gotcha */
  182. return $phpPath . "/fpm/php-fpm";
  183. }
  184. $phpSbinFpmi = $phpPath . "/sbin/php-fpm";
  185. if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) {
  186. return $phpSbinFpmi;
  187. }
  188. }
  189. // try local php-fpm
  190. $fpmPath = dirname(__DIR__) . '/php-fpm';
  191. if (file_exists($fpmPath) && is_executable($fpmPath)) {
  192. return $fpmPath;
  193. }
  194. return false;
  195. }
  196. /**
  197. * Skip test if any of the supplied files does not exist.
  198. *
  199. * @param mixed $files
  200. */
  201. static public function skipIfAnyFileDoesNotExist($files)
  202. {
  203. if ( ! is_array($files)) {
  204. $files = array($files);
  205. }
  206. foreach ($files as $file) {
  207. if ( ! file_exists($file)) {
  208. die("skip File $file does not exist");
  209. }
  210. }
  211. }
  212. /**
  213. * Skip test if config file is invalid.
  214. *
  215. * @param string $configTemplate
  216. *
  217. * @throws \Exception
  218. */
  219. static public function skipIfConfigFails(string $configTemplate)
  220. {
  221. $tester = new self($configTemplate, '', [], self::getCallerFileName());
  222. $testResult = $tester->testConfig();
  223. if ($testResult !== null) {
  224. self::clean(2);
  225. die("skip $testResult");
  226. }
  227. }
  228. /**
  229. * Skip test if IPv6 is not supported.
  230. */
  231. static public function skipIfIPv6IsNotSupported()
  232. {
  233. @stream_socket_client('tcp://[::1]:0', $errno);
  234. if ($errno != 111) {
  235. die('skip IPv6 is not supported.');
  236. }
  237. }
  238. /**
  239. * Skip if running on Travis.
  240. *
  241. * @param $message
  242. */
  243. static public function skipIfTravis($message)
  244. {
  245. if (getenv("TRAVIS")) {
  246. die('skip Travis: ' . $message);
  247. }
  248. }
  249. /**
  250. * Skip if not running as root.
  251. */
  252. static public function skipIfNotRoot()
  253. {
  254. if (getmyuid() != 0) {
  255. die('skip not running as root');
  256. }
  257. }
  258. /**
  259. * Skip if running as root.
  260. */
  261. static public function skipIfRoot()
  262. {
  263. if (getmyuid() == 0) {
  264. die('skip running as root');
  265. }
  266. }
  267. /**
  268. * Skip if posix extension not loaded.
  269. */
  270. static public function skipIfPosixNotLoaded()
  271. {
  272. if ( ! extension_loaded('posix')) {
  273. die('skip posix extension not loaded');
  274. }
  275. }
  276. /**
  277. * Tester constructor.
  278. *
  279. * @param string|array $configTemplate
  280. * @param string $code
  281. * @param array $options
  282. * @param string|null $fileName
  283. * @param bool|null $debug
  284. */
  285. public function __construct(
  286. string|array $configTemplate,
  287. string $code = '',
  288. array $options = [],
  289. string $fileName = null,
  290. bool $debug = null
  291. ) {
  292. $this->configTemplate = $configTemplate;
  293. $this->code = $code;
  294. $this->options = $options;
  295. $this->fileName = $fileName ?: self::getCallerFileName();
  296. $this->debug = $debug !== null ? $debug : (bool)getenv('TEST_FPM_DEBUG');
  297. $this->logReader = new LogReader($this->debug);
  298. $this->logTool = new LogTool($this->logReader, $this->debug);
  299. }
  300. /**
  301. * @param string $ini
  302. */
  303. public function setUserIni(string $ini)
  304. {
  305. $iniFile = __DIR__ . '/.user.ini';
  306. $this->trace('Setting .user.ini file', $ini, isFile: true);
  307. file_put_contents($iniFile, $ini);
  308. }
  309. /**
  310. * Test configuration file.
  311. *
  312. * @return null|string
  313. * @throws \Exception
  314. */
  315. public function testConfig()
  316. {
  317. $configFile = $this->createConfig();
  318. $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1';
  319. $this->trace('Testing config using command', $cmd, true);
  320. exec($cmd, $output, $code);
  321. if ($code) {
  322. return preg_replace("/\[.+?\]/", "", $output[0]);
  323. }
  324. return null;
  325. }
  326. /**
  327. * Start PHP-FPM master process
  328. *
  329. * @param array $extraArgs Command extra arguments.
  330. * @param bool $forceStderr Whether to output to stderr so error log is used.
  331. *
  332. * @return bool
  333. * @throws \Exception
  334. */
  335. public function start(array $extraArgs = [], bool $forceStderr = true)
  336. {
  337. $configFile = $this->createConfig();
  338. $desc = $this->outDesc ? [] : [1 => array('pipe', 'w'), 2 => array('redirect', 1)];
  339. $cmd = [self::findExecutable(), '-F', '-y', $configFile];
  340. if ($forceStderr) {
  341. $cmd[] = '-O';
  342. }
  343. if (getenv('TEST_FPM_RUN_AS_ROOT')) {
  344. $cmd[] = '--allow-to-run-as-root';
  345. }
  346. $cmd = array_merge($cmd, $extraArgs);
  347. $this->trace('Starting FPM using command:', $cmd, true);
  348. $this->masterProcess = proc_open($cmd, $desc, $pipes);
  349. register_shutdown_function(
  350. function ($masterProcess) use ($configFile) {
  351. @unlink($configFile);
  352. if (is_resource($masterProcess)) {
  353. @proc_terminate($masterProcess);
  354. while (proc_get_status($masterProcess)['running']) {
  355. usleep(10000);
  356. }
  357. }
  358. },
  359. $this->masterProcess
  360. );
  361. if ( ! $this->outDesc !== false) {
  362. $this->outDesc = $pipes[1];
  363. $this->logReader->setStreamSource('{{MASTER:OUT}}', $this->outDesc);
  364. }
  365. return true;
  366. }
  367. /**
  368. * Run until needle is found in the log.
  369. *
  370. * @param string $pattern Search pattern to find.
  371. *
  372. * @return bool
  373. * @throws \Exception
  374. */
  375. public function runTill(string $pattern)
  376. {
  377. $this->start();
  378. $found = $this->logTool->expectPattern($pattern);
  379. $this->close(true);
  380. return $found;
  381. }
  382. /**
  383. * Check if connection works.
  384. *
  385. * @param string $host
  386. * @param string|null $successMessage
  387. * @param string|null $errorMessage
  388. * @param int $attempts
  389. * @param int $delay
  390. */
  391. public function checkConnection(
  392. string $host = '127.0.0.1',
  393. string $successMessage = null,
  394. ?string $errorMessage = 'Connection failed',
  395. int $attempts = 20,
  396. int $delay = 50000
  397. ) {
  398. $i = 0;
  399. do {
  400. if ($i > 0 && $delay > 0) {
  401. usleep($delay);
  402. }
  403. $fp = @fsockopen($host, $this->getPort());
  404. } while ((++$i < $attempts) && ! $fp);
  405. if ($fp) {
  406. $this->trace('Checking connection successful');
  407. $this->message($successMessage);
  408. fclose($fp);
  409. } else {
  410. $this->message($errorMessage);
  411. }
  412. }
  413. /**
  414. * Execute request with parameters ordered for better checking.
  415. *
  416. * @param string $address
  417. * @param string|null $successMessage
  418. * @param string|null $errorMessage
  419. * @param string $uri
  420. * @param string $query
  421. * @param array $headers
  422. *
  423. * @return Response
  424. */
  425. public function checkRequest(
  426. string $address,
  427. string $successMessage = null,
  428. string $errorMessage = null,
  429. string $uri = '/ping',
  430. string $query = '',
  431. array $headers = []
  432. ): Response {
  433. return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage);
  434. }
  435. /**
  436. * Execute and check ping request.
  437. *
  438. * @param string $address
  439. * @param string $pingPath
  440. * @param string $pingResponse
  441. */
  442. public function ping(
  443. string $address = '{{ADDR}}',
  444. string $pingResponse = 'pong',
  445. string $pingPath = '/ping'
  446. ) {
  447. $response = $this->request('', [], $pingPath, $address);
  448. $response->expectBody($pingResponse, 'text/plain');
  449. }
  450. /**
  451. * Execute and check status request(s).
  452. *
  453. * @param array $expectedFields
  454. * @param string|null $address
  455. * @param string $statusPath
  456. * @param mixed $formats
  457. *
  458. * @throws \Exception
  459. */
  460. public function status(
  461. array $expectedFields,
  462. string $address = null,
  463. string $statusPath = '/status',
  464. $formats = ['plain', 'html', 'xml', 'json', 'openmetrics']
  465. ) {
  466. if ( ! is_array($formats)) {
  467. $formats = [$formats];
  468. }
  469. require_once "status.inc";
  470. $status = new Status();
  471. foreach ($formats as $format) {
  472. $query = $format === 'plain' ? '' : $format;
  473. $response = $this->request($query, [], $statusPath, $address);
  474. $status->checkStatus($response, $expectedFields, $format);
  475. }
  476. }
  477. /**
  478. * Get request params array.
  479. *
  480. * @param string $query
  481. * @param array $headers
  482. * @param string|null $uri
  483. * @param string|null $scriptFilename
  484. *
  485. * @return array
  486. */
  487. private function getRequestParams(
  488. string $query = '',
  489. array $headers = [],
  490. string $uri = null,
  491. string $scriptFilename = null
  492. ): array {
  493. if (is_null($uri)) {
  494. $uri = $this->makeSourceFile();
  495. }
  496. $params = array_merge(
  497. [
  498. 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
  499. 'REQUEST_METHOD' => 'GET',
  500. 'SCRIPT_FILENAME' => $scriptFilename ?: $uri,
  501. 'SCRIPT_NAME' => $uri,
  502. 'QUERY_STRING' => $query,
  503. 'REQUEST_URI' => $uri . ($query ? '?' . $query : ""),
  504. 'DOCUMENT_URI' => $uri,
  505. 'SERVER_SOFTWARE' => 'php/fcgiclient',
  506. 'REMOTE_ADDR' => '127.0.0.1',
  507. 'REMOTE_PORT' => '7777',
  508. 'SERVER_ADDR' => '127.0.0.1',
  509. 'SERVER_PORT' => '80',
  510. 'SERVER_NAME' => php_uname('n'),
  511. 'SERVER_PROTOCOL' => 'HTTP/1.1',
  512. 'DOCUMENT_ROOT' => __DIR__,
  513. 'CONTENT_TYPE' => '',
  514. 'CONTENT_LENGTH' => 0
  515. ],
  516. $headers
  517. );
  518. return array_filter($params, function ($value) {
  519. return ! is_null($value);
  520. });
  521. }
  522. /**
  523. * Execute request.
  524. *
  525. * @param string $query
  526. * @param array $headers
  527. * @param string|null $uri
  528. * @param string|null $address
  529. * @param string|null $successMessage
  530. * @param string|null $errorMessagereadLimit
  531. * @param bool $connKeepAlive
  532. * @param string|null $scriptFilename = null
  533. * @param bool $expectError
  534. * @param int $readLimit
  535. *
  536. * @return Response
  537. */
  538. public function request(
  539. string $query = '',
  540. array $headers = [],
  541. string $uri = null,
  542. string $address = null,
  543. string $successMessage = null,
  544. string $errorMessage = null,
  545. bool $connKeepAlive = false,
  546. string $scriptFilename = null,
  547. bool $expectError = false,
  548. int $readLimit = -1,
  549. ): Response {
  550. if ($this->hasError()) {
  551. return new Response(null, true);
  552. }
  553. $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename);
  554. $this->trace('Request params', $params);
  555. try {
  556. $this->response = new Response(
  557. $this->getClient($address, $connKeepAlive)->request_data($params, false, $readLimit)
  558. );
  559. if ($expectError) {
  560. $this->error('Expected request error but the request was successful');
  561. } else {
  562. $this->message($successMessage);
  563. }
  564. } catch (\Exception $exception) {
  565. if ($expectError) {
  566. $this->message($successMessage);
  567. } elseif ($errorMessage === null) {
  568. $this->error("Request failed", $exception);
  569. } else {
  570. $this->message($errorMessage);
  571. }
  572. $this->response = new Response();
  573. }
  574. if ($this->debug) {
  575. $this->response->debugOutput();
  576. }
  577. return $this->response;
  578. }
  579. /**
  580. * Execute multiple requests in parallel.
  581. *
  582. * @param int|array $requests
  583. * @param string|null $address
  584. * @param string|null $successMessage
  585. * @param string|null $errorMessage
  586. * @param bool $connKeepAlive
  587. * @param int $readTimeout
  588. *
  589. * @return Response[]
  590. * @throws \Exception
  591. */
  592. public function multiRequest(
  593. int|array $requests,
  594. string $address = null,
  595. string $successMessage = null,
  596. string $errorMessage = null,
  597. bool $connKeepAlive = false,
  598. int $readTimeout = 0
  599. ) {
  600. if (is_numeric($requests)) {
  601. $requests = array_fill(0, $requests, []);
  602. }
  603. if ($this->hasError()) {
  604. return array_map(fn($request) => new Response(null, true), $requests);
  605. }
  606. try {
  607. $connections = array_map(function ($requestData) use ($address, $connKeepAlive) {
  608. $client = $this->getClient($address, $connKeepAlive);
  609. $params = $this->getRequestParams(
  610. $requestData['query'] ?? '',
  611. $requestData['headers'] ?? [],
  612. $requestData['uri'] ?? null
  613. );
  614. return [
  615. 'client' => $client,
  616. 'requestId' => $client->async_request($params, false),
  617. ];
  618. }, $requests);
  619. $responses = array_map(function ($conn) use ($readTimeout) {
  620. $response = new Response($conn['client']->wait_for_response_data($conn['requestId'], $readTimeout));
  621. if ($this->debug) {
  622. $response->debugOutput();
  623. }
  624. return $response;
  625. }, $connections);
  626. $this->message($successMessage);
  627. return $responses;
  628. } catch (\Exception $exception) {
  629. if ($errorMessage === null) {
  630. $this->error("Request failed", $exception);
  631. } else {
  632. $this->message($errorMessage);
  633. }
  634. return array_map(fn($request) => new Response(null, true), $requests);
  635. }
  636. }
  637. /**
  638. * Get client.
  639. *
  640. * @param string $address
  641. * @param bool $keepAlive
  642. *
  643. * @return Client
  644. */
  645. private function getClient(string $address = null, $keepAlive = false): Client
  646. {
  647. $address = $address ? $this->processTemplate($address) : $this->getAddr();
  648. if ($address[0] === '/') { // uds
  649. $host = 'unix://' . $address;
  650. $port = -1;
  651. } elseif ($address[0] === '[') { // ipv6
  652. $addressParts = explode(']:', $address);
  653. $host = $addressParts[0];
  654. if (isset($addressParts[1])) {
  655. $host .= ']';
  656. $port = $addressParts[1];
  657. } else {
  658. $port = $this->getPort();
  659. }
  660. } else { // ipv4
  661. $addressParts = explode(':', $address);
  662. $host = $addressParts[0];
  663. $port = $addressParts[1] ?? $this->getPort();
  664. }
  665. if ( ! $keepAlive) {
  666. return new Client($host, $port);
  667. }
  668. if ( ! isset($this->clients[$host][$port])) {
  669. $client = new Client($host, $port);
  670. $client->setKeepAlive(true);
  671. $this->clients[$host][$port] = $client;
  672. }
  673. return $this->clients[$host][$port];
  674. }
  675. /**
  676. * @return string
  677. */
  678. public function getUser()
  679. {
  680. return get_current_user();
  681. }
  682. /**
  683. * @return string
  684. */
  685. public function getGroup()
  686. {
  687. return get_current_group();
  688. }
  689. /**
  690. * @return int
  691. */
  692. public function getUid()
  693. {
  694. return getmyuid();
  695. }
  696. /**
  697. * @return int
  698. */
  699. public function getGid()
  700. {
  701. return getmygid();
  702. }
  703. /**
  704. * Reload FPM by sending USR2 signal and optionally change config before that.
  705. *
  706. * @param string|array $configTemplate
  707. *
  708. * @return string
  709. * @throws \Exception
  710. */
  711. public function reload($configTemplate = null)
  712. {
  713. if ( ! is_null($configTemplate)) {
  714. self::cleanConfigFiles();
  715. $this->configTemplate = $configTemplate;
  716. $this->createConfig();
  717. }
  718. return $this->signal('USR2');
  719. }
  720. /**
  721. * Reload FPM logs by sending USR1 signal.
  722. *
  723. * @return string
  724. * @throws \Exception
  725. */
  726. public function reloadLogs(): string
  727. {
  728. return $this->signal('USR1');
  729. }
  730. /**
  731. * Send signal to the supplied PID or the server PID.
  732. *
  733. * @param string $signal
  734. * @param int|null $pid
  735. *
  736. * @return string
  737. */
  738. public function signal($signal, int $pid = null)
  739. {
  740. if (is_null($pid)) {
  741. $pid = $this->getPid();
  742. }
  743. $cmd = "kill -$signal $pid";
  744. $this->trace('Sending signal using command', $cmd, true);
  745. return exec("kill -$signal $pid");
  746. }
  747. /**
  748. * Terminate master process
  749. */
  750. public function terminate()
  751. {
  752. proc_terminate($this->masterProcess);
  753. }
  754. /**
  755. * Close all open descriptors and process resources
  756. *
  757. * @param bool $terminate
  758. */
  759. public function close($terminate = false)
  760. {
  761. if ($terminate) {
  762. $this->terminate();
  763. }
  764. proc_close($this->masterProcess);
  765. }
  766. /**
  767. * Create a config file.
  768. *
  769. * @param string $extension
  770. *
  771. * @return string
  772. * @throws \Exception
  773. */
  774. private function createConfig($extension = 'ini')
  775. {
  776. if (is_array($this->configTemplate)) {
  777. $configTemplates = $this->configTemplate;
  778. if ( ! isset($configTemplates['main'])) {
  779. throw new \Exception('The config template array has to have main config');
  780. }
  781. $mainTemplate = $configTemplates['main'];
  782. if ( ! is_dir(self::CONF_DIR)) {
  783. mkdir(self::CONF_DIR);
  784. }
  785. foreach ($this->createPoolConfigs($configTemplates) as $name => $poolConfig) {
  786. $this->makeFile(
  787. 'conf',
  788. $this->processTemplate($poolConfig),
  789. self::CONF_DIR,
  790. $name
  791. );
  792. }
  793. } else {
  794. $mainTemplate = $this->configTemplate;
  795. }
  796. return $this->makeFile($extension, $this->processTemplate($mainTemplate));
  797. }
  798. /**
  799. * Create pool config templates.
  800. *
  801. * @param array $configTemplates
  802. *
  803. * @return array
  804. * @throws \Exception
  805. */
  806. private function createPoolConfigs(array $configTemplates)
  807. {
  808. if ( ! isset($configTemplates['poolTemplate'])) {
  809. unset($configTemplates['main']);
  810. return $configTemplates;
  811. }
  812. $poolTemplate = $configTemplates['poolTemplate'];
  813. $configs = [];
  814. if (isset($configTemplates['count'])) {
  815. $start = $configTemplates['start'] ?? 1;
  816. for ($i = $start; $i < $start + $configTemplates['count']; $i++) {
  817. $configs[$i] = str_replace('%index%', $i, $poolTemplate);
  818. }
  819. } elseif (isset($configTemplates['names'])) {
  820. foreach ($configTemplates['names'] as $name) {
  821. $configs[$name] = str_replace('%name%', $name, $poolTemplate);
  822. }
  823. } else {
  824. throw new \Exception('The config template requires count or names if poolTemplate set');
  825. }
  826. return $configs;
  827. }
  828. /**
  829. * Process template string.
  830. *
  831. * @param string $template
  832. *
  833. * @return string
  834. */
  835. private function processTemplate(string $template)
  836. {
  837. $vars = [
  838. 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC],
  839. 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR],
  840. 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW],
  841. 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID],
  842. 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC],
  843. 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR],
  844. 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW],
  845. 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID],
  846. 'ADDR:IPv4' => ['getAddr', 'ipv4'],
  847. 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'],
  848. 'ADDR:IPv6' => ['getAddr', 'ipv6'],
  849. 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'],
  850. 'ADDR:UDS' => ['getAddr', 'uds'],
  851. 'PORT' => ['getPort', 'ip'],
  852. 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf',
  853. 'USER' => ['getUser'],
  854. 'GROUP' => ['getGroup'],
  855. 'UID' => ['getUid'],
  856. 'GID' => ['getGid'],
  857. 'MASTER:OUT' => 'pipe:1',
  858. 'STDERR' => '/dev/stderr',
  859. 'STDOUT' => '/dev/stdout',
  860. ];
  861. $aliases = [
  862. 'ADDR' => 'ADDR:IPv4',
  863. 'FILE:LOG' => 'FILE:LOG:ERR',
  864. ];
  865. foreach ($aliases as $aliasName => $aliasValue) {
  866. $vars[$aliasName] = $vars[$aliasValue];
  867. }
  868. return preg_replace_callback(
  869. '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/',
  870. function ($matches) use ($vars) {
  871. $varName = $matches[1];
  872. if ( ! isset($vars[$varName])) {
  873. $this->error("Invalid config variable $varName");
  874. return 'INVALID';
  875. }
  876. $pool = $matches[2] ?? 'default';
  877. $varValue = $vars[$varName];
  878. if (is_string($varValue)) {
  879. return $varValue;
  880. }
  881. $functionName = array_shift($varValue);
  882. $varValue[] = $pool;
  883. return call_user_func_array([$this, $functionName], $varValue);
  884. },
  885. $template
  886. );
  887. }
  888. /**
  889. * @param string $type
  890. * @param string $pool
  891. *
  892. * @return string
  893. */
  894. public function getAddr(string $type = 'ipv4', $pool = 'default')
  895. {
  896. $port = $this->getPort($type, $pool, true);
  897. if ($type === 'uds') {
  898. $address = $this->getFile($port . '.sock');
  899. // Socket max path length is 108 on Linux and 104 on BSD,
  900. // so we use the latter
  901. if (strlen($address) <= 104) {
  902. return $address;
  903. }
  904. return sys_get_temp_dir() . '/' .
  905. hash('crc32', dirname($address)) . '-' .
  906. basename($address);
  907. }
  908. return $this->getHost($type) . ':' . $port;
  909. }
  910. /**
  911. * @param string $type
  912. * @param string $pool
  913. * @param bool $useAsId
  914. *
  915. * @return int
  916. */
  917. public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false)
  918. {
  919. if ($type === 'uds' && ! $useAsId) {
  920. return -1;
  921. }
  922. if (isset($this->ports['values'][$pool])) {
  923. return $this->ports['values'][$pool];
  924. }
  925. $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1;
  926. $this->ports['values'][$pool] = $this->ports['last'] = $port;
  927. return $port;
  928. }
  929. /**
  930. * @param string $type
  931. *
  932. * @return string
  933. */
  934. public function getHost(string $type = 'ipv4')
  935. {
  936. switch ($type) {
  937. case 'ipv6-any':
  938. return '[::]';
  939. case 'ipv6':
  940. return '[::1]';
  941. case 'ipv4-any':
  942. return '0.0.0.0';
  943. default:
  944. return '127.0.0.1';
  945. }
  946. }
  947. /**
  948. * Get listen address.
  949. *
  950. * @param string|null $template
  951. *
  952. * @return string
  953. */
  954. public function getListen($template = null)
  955. {
  956. return $template ? $this->processTemplate($template) : $this->getAddr();
  957. }
  958. /**
  959. * Get PID.
  960. *
  961. * @return int
  962. */
  963. public function getPid()
  964. {
  965. $pidFile = $this->getFile('pid');
  966. if ( ! is_file($pidFile)) {
  967. return (int)$this->error("PID file has not been created");
  968. }
  969. $pidContent = file_get_contents($pidFile);
  970. if ( ! is_numeric($pidContent)) {
  971. return (int)$this->error("PID content '$pidContent' is not integer");
  972. }
  973. $this->trace('PID found', $pidContent);
  974. return (int)$pidContent;
  975. }
  976. /**
  977. * Get file path for resource file.
  978. *
  979. * @param string $extension
  980. * @param string|null $dir
  981. * @param string|null $name
  982. *
  983. * @return string
  984. */
  985. private function getFile(string $extension, string $dir = null, string $name = null): string
  986. {
  987. $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension;
  988. return is_null($dir) ? $fileName : $dir . '/' . $fileName;
  989. }
  990. /**
  991. * Get absolute file path for the resource file used by templates.
  992. *
  993. * @param string $extension
  994. *
  995. * @return string
  996. */
  997. private function getAbsoluteFile(string $extension): string
  998. {
  999. return $this->getFile($extension);
  1000. }
  1001. /**
  1002. * Get relative file name for resource file used by templates.
  1003. *
  1004. * @param string $extension
  1005. *
  1006. * @return string
  1007. */
  1008. private function getRelativeFile(string $extension): string
  1009. {
  1010. $fileName = rtrim(basename($this->fileName), '.');
  1011. return $this->getFile($extension, null, $fileName);
  1012. }
  1013. /**
  1014. * Get prefixed file.
  1015. *
  1016. * @param string $extension
  1017. * @param string|null $prefix
  1018. *
  1019. * @return string
  1020. */
  1021. public function getPrefixedFile(string $extension, string $prefix = null): string
  1022. {
  1023. $fileName = rtrim($this->fileName, '.');
  1024. if ( ! is_null($prefix)) {
  1025. $fileName = $prefix . '/' . basename($fileName);
  1026. }
  1027. return $this->getFile($extension, null, $fileName);
  1028. }
  1029. /**
  1030. * Create a resource file.
  1031. *
  1032. * @param string $extension
  1033. * @param string $content
  1034. * @param string|null $dir
  1035. * @param string|null $name
  1036. *
  1037. * @return string
  1038. */
  1039. private function makeFile(
  1040. string $extension,
  1041. string $content = '',
  1042. string $dir = null,
  1043. string $name = null,
  1044. bool $overwrite = true
  1045. ): string {
  1046. $filePath = $this->getFile($extension, $dir, $name);
  1047. if ( ! $overwrite && is_file($filePath)) {
  1048. return $filePath;
  1049. }
  1050. file_put_contents($filePath, $content);
  1051. $this->trace('Created file: ' . $filePath, $content, isFile: true);
  1052. return $filePath;
  1053. }
  1054. /**
  1055. * Create a source code file.
  1056. *
  1057. * @return string
  1058. */
  1059. public function makeSourceFile(): string
  1060. {
  1061. return $this->makeFile('src.php', $this->code, overwrite: false);
  1062. }
  1063. /**
  1064. * @param string|null $msg
  1065. */
  1066. private function message($msg)
  1067. {
  1068. if ($msg !== null) {
  1069. echo "$msg\n";
  1070. }
  1071. }
  1072. /**
  1073. * Display error.
  1074. *
  1075. * @param string $msg
  1076. * @param \Exception|null $exception
  1077. *
  1078. * @return false
  1079. */
  1080. private function error($msg, \Exception $exception = null): bool
  1081. {
  1082. $this->error = 'ERROR: ' . $msg;
  1083. if ($exception) {
  1084. $this->error .= '; EXCEPTION: ' . $exception->getMessage();
  1085. }
  1086. $this->error .= "\n";
  1087. echo $this->error;
  1088. return false;
  1089. }
  1090. /**
  1091. * Check whether any error was set.
  1092. *
  1093. * @return bool
  1094. */
  1095. private function hasError()
  1096. {
  1097. return ! is_null($this->error) || ! is_null($this->logTool->getError());
  1098. }
  1099. /**
  1100. * Expect file with a supplied extension to exist.
  1101. *
  1102. * @param string $extension
  1103. * @param string $prefix
  1104. *
  1105. * @return bool
  1106. */
  1107. public function expectFile(string $extension, $prefix = null)
  1108. {
  1109. $filePath = $this->getPrefixedFile($extension, $prefix);
  1110. if ( ! file_exists($filePath)) {
  1111. return $this->error("The file $filePath does not exist");
  1112. }
  1113. $this->trace('File path exists as expected', $filePath);
  1114. return true;
  1115. }
  1116. /**
  1117. * Expect file with a supplied extension to not exist.
  1118. *
  1119. * @param string $extension
  1120. * @param string $prefix
  1121. *
  1122. * @return bool
  1123. */
  1124. public function expectNoFile(string $extension, $prefix = null)
  1125. {
  1126. $filePath = $this->getPrefixedFile($extension, $prefix);
  1127. if (file_exists($filePath)) {
  1128. return $this->error("The file $filePath exists");
  1129. }
  1130. $this->trace('File path does not exist as expected', $filePath);
  1131. return true;
  1132. }
  1133. /**
  1134. * Expect message to be written to FastCGI error stream.
  1135. *
  1136. * @param string $message
  1137. * @param int $limit
  1138. * @param int $repeat
  1139. */
  1140. public function expectFastCGIErrorMessage(
  1141. string $message,
  1142. int $limit = 1024,
  1143. int $repeat = 0
  1144. ) {
  1145. $this->logTool->setExpectedMessage($message, $limit, $repeat);
  1146. $this->logTool->checkTruncatedMessage($this->response->getErrorData());
  1147. }
  1148. /**
  1149. * Expect reloading lines to be logged.
  1150. *
  1151. * @param int $socketCount
  1152. * @param bool $expectInitialProgressMessage
  1153. * @param bool $expectReloadingMessage
  1154. *
  1155. * @throws \Exception
  1156. */
  1157. public function expectLogReloadingNotices(
  1158. int $socketCount = 1,
  1159. bool $expectInitialProgressMessage = true,
  1160. bool $expectReloadingMessage = true
  1161. ) {
  1162. $this->logTool->expectReloadingLines(
  1163. $socketCount,
  1164. $expectInitialProgressMessage,
  1165. $expectReloadingMessage
  1166. );
  1167. }
  1168. /**
  1169. * Expect reloading lines to be logged.
  1170. *
  1171. * @throws \Exception
  1172. */
  1173. public function expectLogReloadingLogsNotices()
  1174. {
  1175. $this->logTool->expectReloadingLogsLines();
  1176. }
  1177. /**
  1178. * Expect starting lines to be logged.
  1179. * @throws \Exception
  1180. */
  1181. public function expectLogStartNotices()
  1182. {
  1183. $this->logTool->expectStartingLines();
  1184. }
  1185. /**
  1186. * Expect terminating lines to be logged.
  1187. * @throws \Exception
  1188. */
  1189. public function expectLogTerminatingNotices()
  1190. {
  1191. $this->logTool->expectTerminatorLines();
  1192. }
  1193. /**
  1194. * Expect log pattern in logs.
  1195. *
  1196. * @param string $pattern Log pattern
  1197. *
  1198. * @throws \Exception
  1199. */
  1200. public function expectLogPattern(string $pattern)
  1201. {
  1202. $this->logTool->expectPattern($pattern);
  1203. }
  1204. /**
  1205. * Expect log message that can span multiple lines.
  1206. *
  1207. * @param string $message
  1208. * @param int $limit
  1209. * @param int $repeat
  1210. * @param bool $decorated
  1211. * @param bool $wrapped
  1212. *
  1213. * @throws \Exception
  1214. */
  1215. public function expectLogMessage(
  1216. string $message,
  1217. int $limit = 1024,
  1218. int $repeat = 0,
  1219. bool $decorated = true,
  1220. bool $wrapped = true
  1221. ) {
  1222. $this->logTool->setExpectedMessage($message, $limit, $repeat);
  1223. if ($wrapped) {
  1224. $this->logTool->checkWrappedMessage(true, $decorated);
  1225. } else {
  1226. $this->logTool->checkTruncatedMessage();
  1227. }
  1228. }
  1229. /**
  1230. * Expect a single log line.
  1231. *
  1232. * @param string $message The expected message.
  1233. * @param bool $isStdErr Whether it is logged to stderr.
  1234. * @param bool $decorated Whether the log lines are decorated.
  1235. *
  1236. * @return bool
  1237. * @throws \Exception
  1238. */
  1239. public function expectLogLine(
  1240. string $message,
  1241. bool $isStdErr = true,
  1242. bool $decorated = true
  1243. ): bool {
  1244. $messageLen = strlen($message);
  1245. $limit = $messageLen > 1024 ? $messageLen + 16 : 1024;
  1246. $this->logTool->setExpectedMessage($message, $limit);
  1247. return $this->logTool->checkWrappedMessage(false, $decorated, $isStdErr);
  1248. }
  1249. /**
  1250. * Expect log entry.
  1251. *
  1252. * @param string $type The log type
  1253. * @param string $message The expected message
  1254. * @param string|null $pool The pool for pool prefixed log entry
  1255. * @param int $count The number of items
  1256. * @param bool $checkAllLogs Whether to also check past logs.
  1257. *
  1258. * @return bool
  1259. * @throws \Exception
  1260. */
  1261. private function expectLogEntry(
  1262. string $type,
  1263. string $message,
  1264. string $pool = null,
  1265. int $count = 1,
  1266. bool $checkAllLogs = false
  1267. ): bool {
  1268. for ($i = 0; $i < $count; $i++) {
  1269. if ( ! $this->logTool->expectEntry($type, $message, $pool, $checkAllLogs)) {
  1270. return false;
  1271. }
  1272. }
  1273. return true;
  1274. }
  1275. /**
  1276. * Expect a log debug message.
  1277. *
  1278. * @param string $message
  1279. * @param string|null $pool
  1280. * @param int $count
  1281. * @param bool $checkAllLogs Whether to also check past logs.
  1282. *
  1283. * @return bool
  1284. * @throws \Exception
  1285. */
  1286. public function expectLogDebug(
  1287. string $message,
  1288. string $pool = null,
  1289. int $count = 1,
  1290. bool $checkAllLogs = false
  1291. ): bool {
  1292. return $this->expectLogEntry(LogTool::DEBUG, $message, $pool, $count, $checkAllLogs);
  1293. }
  1294. /**
  1295. * Expect a log notice.
  1296. *
  1297. * @param string $message
  1298. * @param string|null $pool
  1299. * @param int $count
  1300. * @param bool $checkAllLogs Whether to also check past logs.
  1301. *
  1302. * @return bool
  1303. * @throws \Exception
  1304. */
  1305. public function expectLogNotice(
  1306. string $message,
  1307. string $pool = null,
  1308. int $count = 1,
  1309. bool $checkAllLogs = false
  1310. ): bool {
  1311. return $this->expectLogEntry(LogTool::NOTICE, $message, $pool, $count, $checkAllLogs);
  1312. }
  1313. /**
  1314. * Expect a log warning.
  1315. *
  1316. * @param string $message
  1317. * @param string|null $pool
  1318. * @param int $count
  1319. * @param bool $checkAllLogs Whether to also check past logs.
  1320. *
  1321. * @return bool
  1322. * @throws \Exception
  1323. */
  1324. public function expectLogWarning(
  1325. string $message,
  1326. string $pool = null,
  1327. int $count = 1,
  1328. bool $checkAllLogs = false
  1329. ): bool {
  1330. return $this->expectLogEntry(LogTool::WARNING, $message, $pool, $count, $checkAllLogs);
  1331. }
  1332. /**
  1333. * Expect a log error.
  1334. *
  1335. * @param string $message
  1336. * @param string|null $pool
  1337. * @param int $count
  1338. * @param bool $checkAllLogs Whether to also check past logs.
  1339. *
  1340. * @return bool
  1341. * @throws \Exception
  1342. */
  1343. public function expectLogError(
  1344. string $message,
  1345. string $pool = null,
  1346. int $count = 1,
  1347. bool $checkAllLogs = false
  1348. ): bool {
  1349. return $this->expectLogEntry(LogTool::ERROR, $message, $pool, $count, $checkAllLogs);
  1350. }
  1351. /**
  1352. * Expect a log alert.
  1353. *
  1354. * @param string $message
  1355. * @param string|null $pool
  1356. * @param int $count
  1357. * @param bool $checkAllLogs Whether to also check past logs.
  1358. *
  1359. * @return bool
  1360. * @throws \Exception
  1361. */
  1362. public function expectLogAlert(
  1363. string $message,
  1364. string $pool = null,
  1365. int $count = 1,
  1366. bool $checkAllLogs = false
  1367. ): bool {
  1368. return $this->expectLogEntry(LogTool::ALERT, $message, $pool, $count, $checkAllLogs);
  1369. }
  1370. /**
  1371. * Expect no log lines to be logged.
  1372. *
  1373. * @return bool
  1374. * @throws \Exception
  1375. */
  1376. public function expectNoLogMessages(): bool
  1377. {
  1378. $logLine = $this->logReader->getLine(timeoutSeconds: 0, timeoutMicroseconds: 1000);
  1379. if ($logLine === "") {
  1380. $logLine = $this->logReader->getLine(timeoutSeconds: 0, timeoutMicroseconds: 1000);
  1381. }
  1382. if ($logLine !== null) {
  1383. return $this->error(
  1384. "Expected no log lines but following line logged: $logLine"
  1385. );
  1386. }
  1387. $this->trace('No log message received as expected');
  1388. return true;
  1389. }
  1390. /**
  1391. * Expect log config options
  1392. *
  1393. * @param array $options
  1394. *
  1395. * @return bool
  1396. * @throws \Exception
  1397. */
  1398. public function expectLogConfigOptions(array $options)
  1399. {
  1400. foreach ($options as $name => $value) {
  1401. $this->expectLogNotice("\s+$name\s=\s$value", checkAllLogs: true);
  1402. }
  1403. return true;
  1404. }
  1405. /**
  1406. * Print content of access log.
  1407. */
  1408. public function printAccessLog()
  1409. {
  1410. $accessLog = $this->getFile('acc.log');
  1411. if (is_file($accessLog)) {
  1412. print file_get_contents($accessLog);
  1413. }
  1414. }
  1415. /**
  1416. * Read all log entries.
  1417. *
  1418. * @param string $type The log type
  1419. * @param string $message The expected message
  1420. * @param string|null $pool The pool for pool prefixed log entry
  1421. *
  1422. * @return bool
  1423. * @throws \Exception
  1424. */
  1425. public function readAllLogEntries(string $type, string $message, string $pool = null): bool
  1426. {
  1427. return $this->logTool->readAllEntries($type, $message, $pool);
  1428. }
  1429. /**
  1430. * Read all log entries.
  1431. *
  1432. * @param string $message The expected message
  1433. * @param string|null $pool The pool for pool prefixed log entry
  1434. *
  1435. * @return bool
  1436. * @throws \Exception
  1437. */
  1438. public function readAllLogNotices(string $message, string $pool = null): bool
  1439. {
  1440. return $this->readAllLogEntries(LogTool::NOTICE, $message, $pool);
  1441. }
  1442. /**
  1443. * Switch the logs source.
  1444. *
  1445. * @param string $source The source file path or name if log is a pipe.
  1446. *
  1447. * @throws \Exception
  1448. */
  1449. public function switchLogSource(string $source)
  1450. {
  1451. $this->trace('Switching log descriptor to:', $source);
  1452. $this->logReader->setFileSource($source, $this->processTemplate($source));
  1453. }
  1454. /**
  1455. * Trace execution by printing supplied message only in debug mode.
  1456. *
  1457. * @param string $title Trace title to print if supplied.
  1458. * @param string|array|null $message Message to print.
  1459. * @param bool $isCommand Whether message is a command array.
  1460. */
  1461. private function trace(
  1462. string $title,
  1463. string|array $message = null,
  1464. bool $isCommand = false,
  1465. bool $isFile = false
  1466. ): void {
  1467. if ($this->debug) {
  1468. echo "\n";
  1469. echo ">>> $title\n";
  1470. if (is_array($message)) {
  1471. if ($isCommand) {
  1472. echo implode(' ', $message) . "\n";
  1473. } else {
  1474. print_r($message);
  1475. }
  1476. } elseif ($message !== null) {
  1477. if ($isFile) {
  1478. $this->logReader->printSeparator();
  1479. }
  1480. echo $message . "\n";
  1481. if ($isFile) {
  1482. $this->logReader->printSeparator();
  1483. }
  1484. }
  1485. }
  1486. }
  1487. }