1721 lines
46 KiB
PHP
1721 lines
46 KiB
PHP
<?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 bool
|
|
*/
|
|
private bool $daemonized;
|
|
|
|
/**
|
|
* @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.
|
|
* @param bool $daemonize Whether to start FPM daemonized
|
|
* @param array $extensions List of extension to add if shared build used.
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function start(
|
|
array $extraArgs = [],
|
|
bool $forceStderr = true,
|
|
bool $daemonize = false,
|
|
array $extensions = []
|
|
) {
|
|
$configFile = $this->createConfig();
|
|
$desc = $this->outDesc ? [] : [1 => array('pipe', 'w'), 2 => array('redirect', 1)];
|
|
|
|
$cmd = [self::findExecutable(), '-y', $configFile];
|
|
|
|
if ($forceStderr) {
|
|
$cmd[] = '-O';
|
|
}
|
|
|
|
$this->daemonized = $daemonize;
|
|
if ( ! $daemonize) {
|
|
$cmd[] = '-F';
|
|
}
|
|
|
|
$extensionDir = getenv('TEST_FPM_EXTENSION_DIR');
|
|
if ($extensionDir) {
|
|
$cmd[] = '-dextension_dir=' . $extensionDir;
|
|
foreach ($extensions as $extension) {
|
|
$cmd[] = '-dextension=' . $extension;
|
|
}
|
|
}
|
|
|
|
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);
|
|
if ($daemonize) {
|
|
$this->switchLogSource('{{FILE:LOG}}');
|
|
}
|
|
}
|
|
|
|
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']
|
|
) {
|
|
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
|
|
* @param string|null $stdin
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getRequestParams(
|
|
string $query = '',
|
|
array $headers = [],
|
|
string $uri = null,
|
|
string $scriptFilename = null,
|
|
?string $stdin = null
|
|
): array {
|
|
if (is_null($uri)) {
|
|
$uri = $this->makeSourceFile();
|
|
}
|
|
|
|
$params = array_merge(
|
|
[
|
|
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
|
|
'REQUEST_METHOD' => is_null($stdin) ? 'GET' : 'POST',
|
|
'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' => strlen($stdin ?? "") // Default to 0
|
|
],
|
|
$headers
|
|
);
|
|
|
|
return array_filter($params, function ($value) {
|
|
return ! is_null($value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse stdin and generate data for multipart config.
|
|
*
|
|
* @param array $stdin
|
|
* @param array $headers
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
private function parseStdin(array $stdin, array &$headers)
|
|
{
|
|
$parts = $stdin['parts'] ?? null;
|
|
if (empty($parts)) {
|
|
throw new \Exception('The stdin array needs to contain parts');
|
|
}
|
|
$boundary = $stdin['boundary'] ?? 'AaB03x';
|
|
if ( ! isset($headers['CONTENT_TYPE'])) {
|
|
$headers['CONTENT_TYPE'] = 'multipart/form-data; boundary=' . $boundary;
|
|
}
|
|
$count = $parts['count'] ?? null;
|
|
if ( ! is_null($count)) {
|
|
$dispositionType = $parts['disposition'] ?? 'form-data';
|
|
$dispositionParam = $parts['param'] ?? 'name';
|
|
$namePrefix = $parts['prefix'] ?? 'f';
|
|
$nameSuffix = $parts['suffix'] ?? '';
|
|
$value = $parts['value'] ?? 'test';
|
|
$parts = [];
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$parts[] = [
|
|
'disposition' => $dispositionType,
|
|
'param' => $dispositionParam,
|
|
'name' => "$namePrefix$i$nameSuffix",
|
|
'value' => $value
|
|
];
|
|
}
|
|
}
|
|
$out = '';
|
|
$nl = "\r\n";
|
|
foreach ($parts as $part) {
|
|
if (!is_array($part)) {
|
|
$part = ['name' => $part];
|
|
} elseif ( ! isset($part['name'])) {
|
|
throw new \Exception('Each part has to have a name');
|
|
}
|
|
$name = $part['name'];
|
|
$dispositionType = $part['disposition'] ?? 'form-data';
|
|
$dispositionParam = $part['param'] ?? 'name';
|
|
$value = $part['value'] ?? 'test';
|
|
$partHeaders = $part['headers'] ?? [];
|
|
|
|
$out .= "--$boundary$nl";
|
|
$out .= "Content-disposition: $dispositionType; $dispositionParam=\"$name\"$nl";
|
|
foreach ($partHeaders as $headerName => $headerValue) {
|
|
$out .= "$headerName: $headerValue$nl";
|
|
}
|
|
$out .= $nl;
|
|
$out .= "$value$nl";
|
|
}
|
|
$out .= "--$boundary--$nl";
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Execute request.
|
|
*
|
|
* @param string $query
|
|
* @param array $headers
|
|
* @param string|null $uri
|
|
* @param string|null $address
|
|
* @param string|null $successMessage
|
|
* @param string|null $errorMessage
|
|
* @param bool $connKeepAlive
|
|
* @param string|null $scriptFilename = null
|
|
* @param string|array|null $stdin = null
|
|
* @param bool $expectError
|
|
* @param int $readLimit
|
|
*
|
|
* @return Response
|
|
* @throws \Exception
|
|
*/
|
|
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,
|
|
string|array $stdin = null,
|
|
bool $expectError = false,
|
|
int $readLimit = -1,
|
|
): Response {
|
|
if ($this->hasError()) {
|
|
return new Response(null, true);
|
|
}
|
|
|
|
if (is_array($stdin)) {
|
|
$stdin = $this->parseStdin($stdin, $headers);
|
|
}
|
|
|
|
$params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $stdin);
|
|
$this->trace('Request params', $params);
|
|
|
|
try {
|
|
$this->response = new Response(
|
|
$this->getClient($address, $connKeepAlive)->request_data($params, $stdin, $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()
|
|
{
|
|
if ($this->daemonized) {
|
|
$this->signal('TERM');
|
|
} else {
|
|
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 log to be empty.
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogEmpty()
|
|
{
|
|
try {
|
|
$line = $this->logReader->getLine(1, 0, true);
|
|
if ($line === '') {
|
|
$line = $this->logReader->getLine(1, 0, true);
|
|
}
|
|
if ($line !== null) {
|
|
$this->error('Log is not closed and returned line: ' . $line);
|
|
}
|
|
} catch (LogTimoutException $exception) {
|
|
$this->error('Log is not closed and timed out', $exception);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
private function expectLogEntry(
|
|
string $type,
|
|
string $message,
|
|
string $pool = null,
|
|
int $count = 1
|
|
): bool {
|
|
for ($i = 0; $i < $count; $i++) {
|
|
if ( ! $this->logTool->expectEntry($type, $message, $pool)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Expect a log debug message.
|
|
*
|
|
* @param string $message
|
|
* @param string|null $pool
|
|
* @param int $count
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogDebug(string $message, string $pool = null, int $count = 1): bool
|
|
{
|
|
return $this->expectLogEntry(LogTool::DEBUG, $message, $pool, $count);
|
|
}
|
|
|
|
/**
|
|
* Expect a log notice.
|
|
*
|
|
* @param string $message
|
|
* @param string|null $pool
|
|
* @param int $count
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogNotice(string $message, string $pool = null, int $count = 1): bool
|
|
{
|
|
return $this->expectLogEntry(LogTool::NOTICE, $message, $pool, $count);
|
|
}
|
|
|
|
/**
|
|
* Expect a log warning.
|
|
*
|
|
* @param string $message
|
|
* @param string|null $pool
|
|
* @param int $count
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogWarning(string $message, string $pool = null, int $count = 1): bool
|
|
{
|
|
return $this->expectLogEntry(LogTool::WARNING, $message, $pool, $count);
|
|
}
|
|
|
|
/**
|
|
* Expect a log error.
|
|
*
|
|
* @param string $message
|
|
* @param string|null $pool
|
|
* @param int $count
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogError(string $message, string $pool = null, int $count = 1): bool
|
|
{
|
|
return $this->expectLogEntry(LogTool::ERROR, $message, $pool, $count);
|
|
}
|
|
|
|
/**
|
|
* Expect a log alert.
|
|
*
|
|
* @param string $message
|
|
* @param string|null $pool
|
|
* @param int $count
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function expectLogAlert(string $message, string $pool = null, int $count = 1): bool
|
|
{
|
|
return $this->expectLogEntry(LogTool::ALERT, $message, $pool, $count);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|