logreader.inc 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. namespace FPM;
  3. class LogReader
  4. {
  5. /**
  6. * Log debugging.
  7. *
  8. * @var bool
  9. */
  10. private bool $debug;
  11. /**
  12. * Log descriptor.
  13. *
  14. * @var string|null
  15. */
  16. private ?string $currentSourceName;
  17. /**
  18. * Log descriptors.
  19. *
  20. * @var LogSource[]
  21. */
  22. private array $sources = [];
  23. /**
  24. * Log reader constructor.
  25. *
  26. * @param bool $debug
  27. */
  28. public function __construct(bool $debug = false)
  29. {
  30. $this->debug = $debug;
  31. }
  32. /**
  33. * Returns log descriptor source.
  34. *
  35. * @return LogSource
  36. * @throws \Exception
  37. */
  38. private function getSource(): LogSource
  39. {
  40. if ( ! $this->currentSourceName) {
  41. throw new \Exception('Log descriptor is not set');
  42. }
  43. return $this->sources[$this->currentSourceName];
  44. }
  45. /**
  46. * Set current stream source and create it if it does not exist.
  47. *
  48. * @param string $name Stream name.
  49. * @param resource $stream The actual stream.
  50. */
  51. public function setStreamSource(string $name, $stream)
  52. {
  53. $this->currentSourceName = $name;
  54. if ( ! isset($this->sources[$name])) {
  55. $this->sources[$name] = new LogStreamSource($stream);
  56. }
  57. }
  58. /**
  59. * Set file source as current and create it if it does not exist.
  60. *
  61. * @param string $name Source name.
  62. * @param string $filePath Source file path.s
  63. */
  64. public function setFileSource(string $name, string $filePath)
  65. {
  66. $this->currentSourceName = $name;
  67. if ( ! isset($this->sources[$name])) {
  68. $this->sources[$name] = new LogFileSource($filePath);
  69. }
  70. }
  71. /**
  72. * Get a single log line.
  73. *
  74. * @param int $timeoutSeconds
  75. * @param int $timeoutMicroseconds
  76. *
  77. * @return null|string
  78. * @throws \Exception
  79. */
  80. public function getLine(int $timeoutSeconds = 3, int $timeoutMicroseconds = 0): ?string
  81. {
  82. $line = $this->getSource()->getLine($timeoutSeconds, $timeoutMicroseconds);
  83. $this->trace(is_null($line) ? "LINE - null" : "LINE: $line");
  84. return $line;
  85. }
  86. /**
  87. * Print separation line.
  88. */
  89. public function printSeparator(): void
  90. {
  91. echo str_repeat('-', 68) . "\n";
  92. }
  93. /**
  94. * Print all logs.
  95. */
  96. public function printLogs(): void
  97. {
  98. $hasMultipleDescriptors = count($this->sources) > 1;
  99. echo "LOGS:\n";
  100. foreach ($this->sources as $name => $source) {
  101. if ($hasMultipleDescriptors) {
  102. echo ">>> source: $name\n";
  103. }
  104. $this->printSeparator();
  105. foreach ($source->getAllLines() as $line) {
  106. echo $line;
  107. }
  108. $this->printSeparator();
  109. }
  110. }
  111. /**
  112. * Print error and logs.
  113. *
  114. * @param string|null $errorMessage Error message to print before the logs.
  115. *
  116. * @return false
  117. */
  118. private function printError(?string $errorMessage): bool
  119. {
  120. if (is_null($errorMessage)) {
  121. return false;
  122. }
  123. echo "ERROR: " . $errorMessage . "\n\n";
  124. $this->printLogs();
  125. echo "\n";
  126. return false;
  127. }
  128. /**
  129. * Read log until matcher matches the log message or there are no more logs.
  130. *
  131. * @param callable $matcher Callback to identify a match
  132. * @param string|null $notFoundMessage Error message if matcher does not succeed.
  133. * @param bool $checkAllLogs Whether to also check past logs.
  134. * @param int $timeoutSeconds Timeout in seconds for reading of all messages.
  135. * @param int $timeoutMicroseconds Additional timeout in microseconds for reading of all messages.
  136. *
  137. * @return bool
  138. * @throws \Exception
  139. */
  140. public function readUntil(
  141. callable $matcher,
  142. string $notFoundMessage = null,
  143. bool $checkAllLogs = false,
  144. int $timeoutSeconds = 3,
  145. int $timeoutMicroseconds = 0
  146. ): bool {
  147. $startTime = microtime(true);
  148. $endTime = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
  149. if ($checkAllLogs) {
  150. foreach ($this->getSource()->getAllLines() as $line) {
  151. if ($matcher($line)) {
  152. return true;
  153. }
  154. }
  155. }
  156. do {
  157. if (microtime(true) > $endTime) {
  158. return $this->printError($notFoundMessage);
  159. }
  160. $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds);
  161. if ($line === null || microtime(true) > $endTime) {
  162. return $this->printError($notFoundMessage);
  163. }
  164. } while ( ! $matcher($line));
  165. return true;
  166. }
  167. /**
  168. * Print tracing message - only in debug .
  169. *
  170. * @param string $msg Message to print.
  171. */
  172. private function trace(string $msg): void
  173. {
  174. if ($this->debug) {
  175. print "LogReader - $msg";
  176. }
  177. }
  178. }
  179. abstract class LogSource
  180. {
  181. /**
  182. * Get single line from the source.
  183. *
  184. * @param int $timeoutSeconds Read timeout in seconds
  185. * @param int $timeoutMicroseconds Read timeout in microseconds
  186. *
  187. * @return string|null
  188. */
  189. public abstract function getLine(int $timeoutSeconds, int $timeoutMicroseconds): ?string;
  190. /**
  191. * Get all lines that has been returned by getLine() method.
  192. *
  193. * @return string[]
  194. */
  195. public abstract function getAllLines(): array;
  196. }
  197. class LogStreamSource extends LogSource
  198. {
  199. /**
  200. * @var resource
  201. */
  202. private $stream;
  203. /**
  204. * @var array
  205. */
  206. private array $lines = [];
  207. public function __construct($stream)
  208. {
  209. $this->stream = $stream;
  210. }
  211. /**
  212. * Get single line from the stream.
  213. *
  214. * @param int $timeoutSeconds Read timeout in seconds
  215. * @param int $timeoutMicroseconds Read timeout in microseconds
  216. *
  217. * @return string|null
  218. */
  219. public function getLine(int $timeoutSeconds, int $timeoutMicroseconds): ?string
  220. {
  221. if (feof($this->stream)) {
  222. return null;
  223. }
  224. $read = [$this->stream];
  225. $write = null;
  226. $except = null;
  227. if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) {
  228. $line = fgets($this->stream);
  229. $this->lines[] = $line;
  230. return $line;
  231. } else {
  232. return null;
  233. }
  234. }
  235. /**
  236. * Get all stream read lines.
  237. *
  238. * @return string[]
  239. */
  240. public function getAllLines(): array
  241. {
  242. return $this->lines;
  243. }
  244. }
  245. class LogFileSource extends LogSource
  246. {
  247. /**
  248. * @var string
  249. */
  250. private string $filePath;
  251. /**
  252. * @var int
  253. */
  254. private int $position;
  255. /**
  256. * @var array
  257. */
  258. private array $lines = [];
  259. public function __construct(string $filePath)
  260. {
  261. $this->filePath = $filePath;
  262. $this->position = 0;
  263. }
  264. /**
  265. * Get single line from the file.
  266. *
  267. * @param int $timeoutSeconds Read timeout in seconds
  268. * @param int $timeoutMicroseconds Read timeout in microseconds
  269. *
  270. * @return string|null
  271. */
  272. public function getLine(int $timeoutSeconds, int $timeoutMicroseconds): ?string
  273. {
  274. $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
  275. while ($this->position >= count($this->lines)) {
  276. if (is_file($this->filePath)) {
  277. $lines = file($this->filePath);
  278. if ($lines === false) {
  279. return null;
  280. }
  281. $this->lines = $lines;
  282. if ($this->position < count($lines)) {
  283. break;
  284. }
  285. }
  286. usleep(50_000);
  287. if (microtime(true) > $endTime) {
  288. return null;
  289. }
  290. }
  291. return $this->lines[$this->position++];
  292. }
  293. /**
  294. * Get all returned lines from the file.
  295. *
  296. * @return string[]
  297. */
  298. public function getAllLines(): array
  299. {
  300. return array_slice($this->lines, 0, $this->position);
  301. }
  302. }