Monitor.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. /**
  3. * This file is part of webman.
  4. *
  5. * Licensed under The MIT License
  6. * For full copyright and license information, please see the MIT-LICENSE.txt
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @author walkor<walkor@workerman.net>
  10. * @copyright walkor<walkor@workerman.net>
  11. * @link http://www.workerman.net/
  12. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. namespace app\process;
  15. use FilesystemIterator;
  16. use RecursiveDirectoryIterator;
  17. use RecursiveIteratorIterator;
  18. use SplFileInfo;
  19. use Workerman\Timer;
  20. use Workerman\Worker;
  21. /**
  22. * Class FileMonitor
  23. * @package process
  24. */
  25. class Monitor
  26. {
  27. /**
  28. * @var array
  29. */
  30. protected array $paths = [];
  31. /**
  32. * @var array
  33. */
  34. protected array $extensions = [];
  35. /**
  36. * @var array
  37. */
  38. protected array $loadedFiles = [];
  39. /**
  40. * @var int
  41. */
  42. protected int $ppid = 0;
  43. /**
  44. * Pause monitor
  45. * @return void
  46. */
  47. public static function pause(): void
  48. {
  49. file_put_contents(static::lockFile(), time());
  50. }
  51. /**
  52. * Resume monitor
  53. * @return void
  54. */
  55. public static function resume(): void
  56. {
  57. clearstatcache();
  58. if (is_file(static::lockFile())) {
  59. unlink(static::lockFile());
  60. }
  61. }
  62. /**
  63. * Whether monitor is paused
  64. * @return bool
  65. */
  66. public static function isPaused(): bool
  67. {
  68. clearstatcache();
  69. return file_exists(static::lockFile());
  70. }
  71. /**
  72. * Lock file
  73. * @return string
  74. */
  75. protected static function lockFile(): string
  76. {
  77. return runtime_path('monitor.lock');
  78. }
  79. /**
  80. * FileMonitor constructor.
  81. * @param $monitorDir
  82. * @param $monitorExtensions
  83. * @param array $options
  84. */
  85. public function __construct($monitorDir, $monitorExtensions, array $options = [])
  86. {
  87. $this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
  88. static::resume();
  89. $this->paths = (array)$monitorDir;
  90. $this->extensions = $monitorExtensions;
  91. foreach (get_included_files() as $index => $file) {
  92. $this->loadedFiles[$file] = $index;
  93. if (strpos($file, 'webman-framework/src/support/App.php')) {
  94. break;
  95. }
  96. }
  97. if (!Worker::getAllWorkers()) {
  98. return;
  99. }
  100. $disableFunctions = explode(',', ini_get('disable_functions'));
  101. if (in_array('exec', $disableFunctions, true)) {
  102. echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
  103. } else {
  104. if ($options['enable_file_monitor'] ?? true) {
  105. Timer::add(1, function () {
  106. $this->checkAllFilesChange();
  107. });
  108. }
  109. }
  110. $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
  111. if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
  112. Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
  113. }
  114. }
  115. /**
  116. * @param $monitorDir
  117. * @return bool
  118. */
  119. public function checkFilesChange($monitorDir): bool
  120. {
  121. static $lastMtime, $tooManyFilesCheck;
  122. if (!$lastMtime) {
  123. $lastMtime = time();
  124. }
  125. clearstatcache();
  126. if (!is_dir($monitorDir)) {
  127. if (!is_file($monitorDir)) {
  128. return false;
  129. }
  130. $iterator = [new SplFileInfo($monitorDir)];
  131. } else {
  132. // recursive traversal directory
  133. $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
  134. $iterator = new RecursiveIteratorIterator($dirIterator);
  135. }
  136. $count = 0;
  137. foreach ($iterator as $file) {
  138. $count ++;
  139. /** var SplFileInfo $file */
  140. if (is_dir($file->getRealPath())) {
  141. continue;
  142. }
  143. // check mtime
  144. if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
  145. $lastMtime = $file->getMTime();
  146. if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
  147. echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
  148. continue;
  149. }
  150. $var = 0;
  151. exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
  152. if ($var) {
  153. continue;
  154. }
  155. // send SIGUSR1 signal to master process for reload
  156. if (DIRECTORY_SEPARATOR === '/') {
  157. if ($masterPid = $this->getMasterPid()) {
  158. echo $file . " updated and reload\n";
  159. posix_kill($masterPid, SIGUSR1);
  160. } else {
  161. echo "Master process has gone away and can not reload\n";
  162. }
  163. return true;
  164. }
  165. echo $file . " updated and reload\n";
  166. return true;
  167. }
  168. }
  169. if (!$tooManyFilesCheck && $count > 1000) {
  170. echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
  171. $tooManyFilesCheck = 1;
  172. }
  173. return false;
  174. }
  175. /**
  176. * @return int
  177. */
  178. public function getMasterPid(): int
  179. {
  180. if ($this->ppid === 0) {
  181. return 0;
  182. }
  183. if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
  184. echo "Master process has gone away\n";
  185. return $this->ppid = 0;
  186. }
  187. if (PHP_OS_FAMILY !== 'Linux') {
  188. return $this->ppid;
  189. }
  190. $cmdline = "/proc/$this->ppid/cmdline";
  191. if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
  192. // Process not exist
  193. $this->ppid = 0;
  194. }
  195. return $this->ppid;
  196. }
  197. /**
  198. * @return bool
  199. */
  200. public function checkAllFilesChange(): bool
  201. {
  202. if (static::isPaused()) {
  203. return false;
  204. }
  205. foreach ($this->paths as $path) {
  206. if ($this->checkFilesChange($path)) {
  207. return true;
  208. }
  209. }
  210. return false;
  211. }
  212. /**
  213. * @param $memoryLimit
  214. * @return void
  215. */
  216. public function checkMemory($memoryLimit): void
  217. {
  218. if (static::isPaused() || $memoryLimit <= 0) {
  219. return;
  220. }
  221. $masterPid = $this->getMasterPid();
  222. if ($masterPid <= 0) {
  223. echo "Master process has gone away\n";
  224. return;
  225. }
  226. $childrenFile = "/proc/$masterPid/task/$masterPid/children";
  227. if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
  228. return;
  229. }
  230. foreach (explode(' ', $children) as $pid) {
  231. $pid = (int)$pid;
  232. $statusFile = "/proc/$pid/status";
  233. if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
  234. continue;
  235. }
  236. $mem = 0;
  237. if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
  238. $mem = $match[1];
  239. }
  240. $mem = (int)($mem / 1024);
  241. if ($mem >= $memoryLimit) {
  242. posix_kill($pid, SIGINT);
  243. }
  244. }
  245. }
  246. /**
  247. * Get memory limit
  248. * @param $memoryLimit
  249. * @return int
  250. */
  251. protected function getMemoryLimit($memoryLimit): int
  252. {
  253. if ($memoryLimit === 0) {
  254. return 0;
  255. }
  256. $usePhpIni = false;
  257. if (!$memoryLimit) {
  258. $memoryLimit = ini_get('memory_limit');
  259. $usePhpIni = true;
  260. }
  261. if ($memoryLimit == -1) {
  262. return 0;
  263. }
  264. $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
  265. $memoryLimit = (int)$memoryLimit;
  266. if ($unit === 'g') {
  267. $memoryLimit = 1024 * $memoryLimit;
  268. } else if ($unit === 'k') {
  269. $memoryLimit = ($memoryLimit / 1024);
  270. } else if ($unit === 'm') {
  271. $memoryLimit = (int)($memoryLimit);
  272. } else if ($unit === 't') {
  273. $memoryLimit = (1024 * 1024 * $memoryLimit);
  274. } else {
  275. $memoryLimit = ($memoryLimit / (1024 * 1024));
  276. }
  277. if ($memoryLimit < 50) {
  278. $memoryLimit = 50;
  279. }
  280. if ($usePhpIni) {
  281. $memoryLimit = (0.8 * $memoryLimit);
  282. }
  283. return (int)$memoryLimit;
  284. }
  285. }