You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
8.1 KiB

3 years ago
  1. <?php
  2. /*
  3. * This file is part of Psy Shell.
  4. *
  5. * (c) 2012-2020 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Psy\ExecutionLoop;
  11. use Psy\Context;
  12. use Psy\Exception\BreakException;
  13. use Psy\Shell;
  14. /**
  15. * An execution loop listener that forks the process before executing code.
  16. *
  17. * This is awesome, as the session won't die prematurely if user input includes
  18. * a fatal error, such as redeclaring a class or function.
  19. */
  20. class ProcessForker extends AbstractListener
  21. {
  22. private $savegame;
  23. private $up;
  24. private static $pcntlFunctions = [
  25. 'pcntl_fork',
  26. 'pcntl_signal_dispatch',
  27. 'pcntl_signal',
  28. 'pcntl_waitpid',
  29. 'pcntl_wexitstatus',
  30. ];
  31. private static $posixFunctions = [
  32. 'posix_getpid',
  33. 'posix_kill',
  34. ];
  35. /**
  36. * Process forker is supported if pcntl and posix extensions are available.
  37. *
  38. * @return bool
  39. */
  40. public static function isSupported()
  41. {
  42. return self::isPcntlSupported() && !self::disabledPcntlFunctions() && self::isPosixSupported() && !self::disabledPosixFunctions();
  43. }
  44. /**
  45. * Verify that all required pcntl functions are, in fact, available.
  46. */
  47. public static function isPcntlSupported()
  48. {
  49. foreach (self::$pcntlFunctions as $func) {
  50. if (!\function_exists($func)) {
  51. return false;
  52. }
  53. }
  54. return true;
  55. }
  56. /**
  57. * Check whether required pcntl functions are disabled.
  58. */
  59. public static function disabledPcntlFunctions()
  60. {
  61. return self::checkDisabledFunctions(self::$pcntlFunctions);
  62. }
  63. /**
  64. * Verify that all required posix functions are, in fact, available.
  65. */
  66. public static function isPosixSupported()
  67. {
  68. foreach (self::$posixFunctions as $func) {
  69. if (!\function_exists($func)) {
  70. return false;
  71. }
  72. }
  73. return true;
  74. }
  75. /**
  76. * Check whether required posix functions are disabled.
  77. */
  78. public static function disabledPosixFunctions()
  79. {
  80. return self::checkDisabledFunctions(self::$posixFunctions);
  81. }
  82. private static function checkDisabledFunctions(array $functions)
  83. {
  84. return \array_values(\array_intersect($functions, \array_map('strtolower', \array_map('trim', \explode(',', \ini_get('disable_functions'))))));
  85. }
  86. /**
  87. * Forks into a master and a loop process.
  88. *
  89. * The loop process will handle the evaluation of all instructions, then
  90. * return its state via a socket upon completion.
  91. *
  92. * @param Shell $shell
  93. */
  94. public function beforeRun(Shell $shell)
  95. {
  96. list($up, $down) = \stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP);
  97. if (!$up) {
  98. throw new \RuntimeException('Unable to create socket pair');
  99. }
  100. $pid = \pcntl_fork();
  101. if ($pid < 0) {
  102. throw new \RuntimeException('Unable to start execution loop');
  103. } elseif ($pid > 0) {
  104. // This is the main thread. We'll just wait for a while.
  105. // We won't be needing this one.
  106. \fclose($up);
  107. // Wait for a return value from the loop process.
  108. $read = [$down];
  109. $write = null;
  110. $except = null;
  111. do {
  112. $n = @\stream_select($read, $write, $except, null);
  113. if ($n === 0) {
  114. throw new \RuntimeException('Process timed out waiting for execution loop');
  115. }
  116. if ($n === false) {
  117. $err = \error_get_last();
  118. if (!isset($err['message']) || \stripos($err['message'], 'interrupted system call') === false) {
  119. $msg = $err['message'] ?
  120. \sprintf('Error waiting for execution loop: %s', $err['message']) :
  121. 'Error waiting for execution loop';
  122. throw new \RuntimeException($msg);
  123. }
  124. }
  125. } while ($n < 1);
  126. $content = \stream_get_contents($down);
  127. \fclose($down);
  128. if ($content) {
  129. $shell->setScopeVariables(@\unserialize($content));
  130. }
  131. throw new BreakException('Exiting main thread');
  132. }
  133. // This is the child process. It's going to do all the work.
  134. if (!@\cli_set_process_title('psysh (loop)')) {
  135. // Fall back to `setproctitle` if that wasn't succesful.
  136. if (\function_exists('setproctitle')) {
  137. @\setproctitle('psysh (loop)');
  138. }
  139. }
  140. // We won't be needing this one.
  141. \fclose($down);
  142. // Save this; we'll need to close it in `afterRun`
  143. $this->up = $up;
  144. }
  145. /**
  146. * Create a savegame at the start of each loop iteration.
  147. *
  148. * @param Shell $shell
  149. */
  150. public function beforeLoop(Shell $shell)
  151. {
  152. $this->createSavegame();
  153. }
  154. /**
  155. * Clean up old savegames at the end of each loop iteration.
  156. *
  157. * @param Shell $shell
  158. */
  159. public function afterLoop(Shell $shell)
  160. {
  161. // if there's an old savegame hanging around, let's kill it.
  162. if (isset($this->savegame)) {
  163. \posix_kill($this->savegame, \SIGKILL);
  164. \pcntl_signal_dispatch();
  165. }
  166. }
  167. /**
  168. * After the REPL session ends, send the scope variables back up to the main
  169. * thread (if this is a child thread).
  170. *
  171. * @param Shell $shell
  172. */
  173. public function afterRun(Shell $shell)
  174. {
  175. // We're a child thread. Send the scope variables back up to the main thread.
  176. if (isset($this->up)) {
  177. \fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false)));
  178. \fclose($this->up);
  179. \posix_kill(\posix_getpid(), \SIGKILL);
  180. }
  181. }
  182. /**
  183. * Create a savegame fork.
  184. *
  185. * The savegame contains the current execution state, and can be resumed in
  186. * the event that the worker dies unexpectedly (for example, by encountering
  187. * a PHP fatal error).
  188. */
  189. private function createSavegame()
  190. {
  191. // the current process will become the savegame
  192. $this->savegame = \posix_getpid();
  193. $pid = \pcntl_fork();
  194. if ($pid < 0) {
  195. throw new \RuntimeException('Unable to create savegame fork');
  196. } elseif ($pid > 0) {
  197. // we're the savegame now... let's wait and see what happens
  198. \pcntl_waitpid($pid, $status);
  199. // worker exited cleanly, let's bail
  200. if (!\pcntl_wexitstatus($status)) {
  201. \posix_kill(\posix_getpid(), \SIGKILL);
  202. }
  203. // worker didn't exit cleanly, we'll need to have another go
  204. $this->createSavegame();
  205. }
  206. }
  207. /**
  208. * Serialize all serializable return values.
  209. *
  210. * A naïve serialization will run into issues if there is a Closure or
  211. * SimpleXMLElement (among other things) in scope when exiting the execution
  212. * loop. We'll just ignore these unserializable classes, and serialize what
  213. * we can.
  214. *
  215. * @param array $return
  216. *
  217. * @return string
  218. */
  219. private function serializeReturn(array $return)
  220. {
  221. $serializable = [];
  222. foreach ($return as $key => $value) {
  223. // No need to return magic variables
  224. if (Context::isSpecialVariableName($key)) {
  225. continue;
  226. }
  227. // Resources and Closures don't error, but they don't serialize well either.
  228. if (\is_resource($value) || $value instanceof \Closure) {
  229. continue;
  230. }
  231. try {
  232. @\serialize($value);
  233. $serializable[$key] = $value;
  234. } catch (\Throwable $e) {
  235. // we'll just ignore this one...
  236. } catch (\Exception $e) {
  237. // and this one too...
  238. // @todo remove this once we don't support PHP 5.x anymore :)
  239. }
  240. }
  241. return @\serialize($serializable);
  242. }
  243. }