annotate vendor/psy/psysh/src/ExecutionLoop/ProcessForker.php @ 19:fa3358dc1485 tip

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