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 }
|