comparison vendor/psy/psysh/src/ExecutionLoop/ProcessForker.php @ 13:5fb285c0d0e3

Update Drupal core to 8.4.7 via Composer. Security update; I *think* we've been lucky to get away with this so far, as we don't support self-registration which seems to be used by the so-called "drupalgeddon 2" attack that 8.4.5 was vulnerable to.
author Chris Cannam
date Mon, 23 Apr 2018 09:33:26 +0100
parents
children c2387f117808
comparison
equal deleted inserted replaced
12:7a779792577d 13:5fb285c0d0e3
1 <?php
2
3 /*
4 * This file is part of Psy Shell.
5 *
6 * (c) 2012-2018 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
12 namespace Psy\ExecutionLoop;
13
14 use Psy\Context;
15 use Psy\Exception\BreakException;
16 use 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 */
24 class ProcessForker extends AbstractListener
25 {
26 private $savegame;
27 private $up;
28
29 /**
30 * Process forker is supported if pcntl and posix extensions are available.
31 *
32 * @return bool
33 */
34 public static function isSupported()
35 {
36 return function_exists('pcntl_signal') && function_exists('posix_getpid');
37 }
38
39 /**
40 * Forks into a master and a loop process.
41 *
42 * The loop process will handle the evaluation of all instructions, then
43 * return its state via a socket upon completion.
44 *
45 * @param Shell $shell
46 */
47 public function beforeRun(Shell $shell)
48 {
49 list($up, $down) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
50
51 if (!$up) {
52 throw new \RuntimeException('Unable to create socket pair');
53 }
54
55 $pid = pcntl_fork();
56 if ($pid < 0) {
57 throw new \RuntimeException('Unable to start execution loop');
58 } elseif ($pid > 0) {
59 // This is the main thread. We'll just wait for a while.
60
61 // We won't be needing this one.
62 fclose($up);
63
64 // Wait for a return value from the loop process.
65 $read = [$down];
66 $write = null;
67 $except = null;
68 if (stream_select($read, $write, $except, null) === false) {
69 throw new \RuntimeException('Error waiting for execution loop');
70 }
71
72 $content = stream_get_contents($down);
73 fclose($down);
74
75 if ($content) {
76 $shell->setScopeVariables(@unserialize($content));
77 }
78
79 throw new BreakException('Exiting main thread');
80 }
81
82 // This is the child process. It's going to do all the work.
83 if (function_exists('setproctitle')) {
84 setproctitle('psysh (loop)');
85 }
86
87 // We won't be needing this one.
88 fclose($down);
89
90 // Save this; we'll need to close it in `afterRun`
91 $this->up = $up;
92 }
93
94 /**
95 * Create a savegame at the start of each loop iteration.
96 *
97 * @param Shell $shell
98 */
99 public function beforeLoop(Shell $shell)
100 {
101 $this->createSavegame();
102 }
103
104 /**
105 * Clean up old savegames at the end of each loop iteration.
106 *
107 * @param Shell $shell
108 */
109 public function afterLoop(Shell $shell)
110 {
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();
115 }
116 }
117
118 /**
119 * After the REPL session ends, send the scope variables back up to the main
120 * thread (if this is a child thread).
121 *
122 * @param Shell $shell
123 */
124 public function afterRun(Shell $shell)
125 {
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)));
129 fclose($this->up);
130
131 posix_kill(posix_getpid(), SIGKILL);
132 }
133 }
134
135 /**
136 * Create a savegame fork.
137 *
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).
141 */
142 private function createSavegame()
143 {
144 // the current process will become the savegame
145 $this->savegame = posix_getpid();
146
147 $pid = pcntl_fork();
148 if ($pid < 0) {
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);
153
154 // worker exited cleanly, let's bail
155 if (!pcntl_wexitstatus($status)) {
156 posix_kill(posix_getpid(), SIGKILL);
157 }
158
159 // worker didn't exit cleanly, we'll need to have another go
160 $this->createSavegame();
161 }
162 }
163
164 /**
165 * Serialize all serializable return values.
166 *
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
170 * we can.
171 *
172 * @param array $return
173 *
174 * @return string
175 */
176 private function serializeReturn(array $return)
177 {
178 $serializable = [];
179
180 foreach ($return as $key => $value) {
181 // No need to return magic variables
182 if (Context::isSpecialVariableName($key)) {
183 continue;
184 }
185
186 // Resources and Closures don't error, but they don't serialize well either.
187 if (is_resource($value) || $value instanceof \Closure) {
188 continue;
189 }
190
191 try {
192 @serialize($value);
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 :)
199 }
200 }
201
202 return @serialize($serializable);
203 }
204 }