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