4 * This file is part of Psy Shell.
6 * (c) 2012-2018 Justin Hileman
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Psy\ExecutionLoop;
15 use Psy\Exception\BreakException;
19 * An execution loop listener that forks the process before executing code.
21 * This is awesome, as the session won't die prematurely if user input includes
22 * a fatal error, such as redeclaring a class or function.
24 class ProcessForker extends AbstractListener
30 * Process forker is supported if pcntl and posix extensions are available.
34 public static function isSupported()
36 return function_exists('pcntl_signal') && function_exists('posix_getpid');
40 * Forks into a master and a loop process.
42 * The loop process will handle the evaluation of all instructions, then
43 * return its state via a socket upon completion.
47 public function beforeRun(Shell $shell)
49 list($up, $down) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
52 throw new \RuntimeException('Unable to create socket pair');
57 throw new \RuntimeException('Unable to start execution loop');
59 // This is the main thread. We'll just wait for a while.
61 // We won't be needing this one.
64 // Wait for a return value from the loop process.
68 if (stream_select($read, $write, $except, null) === false) {
69 throw new \RuntimeException('Error waiting for execution loop');
72 $content = stream_get_contents($down);
76 $shell->setScopeVariables(@unserialize($content));
79 throw new BreakException('Exiting main thread');
82 // This is the child process. It's going to do all the work.
83 if (function_exists('setproctitle')) {
84 setproctitle('psysh (loop)');
87 // We won't be needing this one.
90 // Save this; we'll need to close it in `afterRun`
95 * Create a savegame at the start of each loop iteration.
99 public function beforeLoop(Shell $shell)
101 $this->createSavegame();
105 * Clean up old savegames at the end of each loop iteration.
107 * @param Shell $shell
109 public function afterLoop(Shell $shell)
111 // if there's an old savegame hanging around, let's kill it.
112 if (isset($this->savegame)) {
113 posix_kill($this->savegame, SIGKILL);
114 pcntl_signal_dispatch();
119 * After the REPL session ends, send the scope variables back up to the main
120 * thread (if this is a child thread).
122 * @param Shell $shell
124 public function afterRun(Shell $shell)
126 // We're a child thread. Send the scope variables back up to the main thread.
127 if (isset($this->up)) {
128 fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false)));
131 posix_kill(posix_getpid(), SIGKILL);
136 * Create a savegame fork.
138 * The savegame contains the current execution state, and can be resumed in
139 * the event that the worker dies unexpectedly (for example, by encountering
140 * a PHP fatal error).
142 private function createSavegame()
144 // the current process will become the savegame
145 $this->savegame = posix_getpid();
149 throw new \RuntimeException('Unable to create savegame fork');
150 } elseif ($pid > 0) {
151 // we're the savegame now... let's wait and see what happens
152 pcntl_waitpid($pid, $status);
154 // worker exited cleanly, let's bail
155 if (!pcntl_wexitstatus($status)) {
156 posix_kill(posix_getpid(), SIGKILL);
159 // worker didn't exit cleanly, we'll need to have another go
160 $this->createSavegame();
165 * Serialize all serializable return values.
167 * A naïve serialization will run into issues if there is a Closure or
168 * SimpleXMLElement (among other things) in scope when exiting the execution
169 * loop. We'll just ignore these unserializable classes, and serialize what
172 * @param array $return
176 private function serializeReturn(array $return)
180 foreach ($return as $key => $value) {
181 // No need to return magic variables
182 if (Context::isSpecialVariableName($key)) {
186 // Resources and Closures don't error, but they don't serialize well either.
187 if (is_resource($value) || $value instanceof \Closure) {
193 $serializable[$key] = $value;
194 } catch (\Throwable $e) {
195 // we'll just ignore this one...
196 } catch (\Exception $e) {
197 // and this one too...
198 // @todo remove this once we don't support PHP 5.x anymore :)
202 return @serialize($serializable);