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@13
|
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@13
|
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@13
|
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@13
|
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@13
|
68 if (stream_select($read, $write, $except, null) === false) {
|
Chris@13
|
69 throw new \RuntimeException('Error waiting for execution loop');
|
Chris@13
|
70 }
|
Chris@13
|
71
|
Chris@13
|
72 $content = stream_get_contents($down);
|
Chris@13
|
73 fclose($down);
|
Chris@13
|
74
|
Chris@13
|
75 if ($content) {
|
Chris@13
|
76 $shell->setScopeVariables(@unserialize($content));
|
Chris@13
|
77 }
|
Chris@13
|
78
|
Chris@13
|
79 throw new BreakException('Exiting main thread');
|
Chris@13
|
80 }
|
Chris@13
|
81
|
Chris@13
|
82 // This is the child process. It's going to do all the work.
|
Chris@13
|
83 if (function_exists('setproctitle')) {
|
Chris@13
|
84 setproctitle('psysh (loop)');
|
Chris@13
|
85 }
|
Chris@13
|
86
|
Chris@13
|
87 // We won't be needing this one.
|
Chris@13
|
88 fclose($down);
|
Chris@13
|
89
|
Chris@13
|
90 // Save this; we'll need to close it in `afterRun`
|
Chris@13
|
91 $this->up = $up;
|
Chris@13
|
92 }
|
Chris@13
|
93
|
Chris@13
|
94 /**
|
Chris@13
|
95 * Create a savegame at the start of each loop iteration.
|
Chris@13
|
96 *
|
Chris@13
|
97 * @param Shell $shell
|
Chris@13
|
98 */
|
Chris@13
|
99 public function beforeLoop(Shell $shell)
|
Chris@13
|
100 {
|
Chris@13
|
101 $this->createSavegame();
|
Chris@13
|
102 }
|
Chris@13
|
103
|
Chris@13
|
104 /**
|
Chris@13
|
105 * Clean up old savegames at the end of each loop iteration.
|
Chris@13
|
106 *
|
Chris@13
|
107 * @param Shell $shell
|
Chris@13
|
108 */
|
Chris@13
|
109 public function afterLoop(Shell $shell)
|
Chris@13
|
110 {
|
Chris@13
|
111 // if there's an old savegame hanging around, let's kill it.
|
Chris@13
|
112 if (isset($this->savegame)) {
|
Chris@13
|
113 posix_kill($this->savegame, SIGKILL);
|
Chris@13
|
114 pcntl_signal_dispatch();
|
Chris@13
|
115 }
|
Chris@13
|
116 }
|
Chris@13
|
117
|
Chris@13
|
118 /**
|
Chris@13
|
119 * After the REPL session ends, send the scope variables back up to the main
|
Chris@13
|
120 * thread (if this is a child thread).
|
Chris@13
|
121 *
|
Chris@13
|
122 * @param Shell $shell
|
Chris@13
|
123 */
|
Chris@13
|
124 public function afterRun(Shell $shell)
|
Chris@13
|
125 {
|
Chris@13
|
126 // We're a child thread. Send the scope variables back up to the main thread.
|
Chris@13
|
127 if (isset($this->up)) {
|
Chris@13
|
128 fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false)));
|
Chris@13
|
129 fclose($this->up);
|
Chris@13
|
130
|
Chris@13
|
131 posix_kill(posix_getpid(), SIGKILL);
|
Chris@13
|
132 }
|
Chris@13
|
133 }
|
Chris@13
|
134
|
Chris@13
|
135 /**
|
Chris@13
|
136 * Create a savegame fork.
|
Chris@13
|
137 *
|
Chris@13
|
138 * The savegame contains the current execution state, and can be resumed in
|
Chris@13
|
139 * the event that the worker dies unexpectedly (for example, by encountering
|
Chris@13
|
140 * a PHP fatal error).
|
Chris@13
|
141 */
|
Chris@13
|
142 private function createSavegame()
|
Chris@13
|
143 {
|
Chris@13
|
144 // the current process will become the savegame
|
Chris@13
|
145 $this->savegame = posix_getpid();
|
Chris@13
|
146
|
Chris@13
|
147 $pid = pcntl_fork();
|
Chris@13
|
148 if ($pid < 0) {
|
Chris@13
|
149 throw new \RuntimeException('Unable to create savegame fork');
|
Chris@13
|
150 } elseif ($pid > 0) {
|
Chris@13
|
151 // we're the savegame now... let's wait and see what happens
|
Chris@13
|
152 pcntl_waitpid($pid, $status);
|
Chris@13
|
153
|
Chris@13
|
154 // worker exited cleanly, let's bail
|
Chris@13
|
155 if (!pcntl_wexitstatus($status)) {
|
Chris@13
|
156 posix_kill(posix_getpid(), SIGKILL);
|
Chris@13
|
157 }
|
Chris@13
|
158
|
Chris@13
|
159 // worker didn't exit cleanly, we'll need to have another go
|
Chris@13
|
160 $this->createSavegame();
|
Chris@13
|
161 }
|
Chris@13
|
162 }
|
Chris@13
|
163
|
Chris@13
|
164 /**
|
Chris@13
|
165 * Serialize all serializable return values.
|
Chris@13
|
166 *
|
Chris@13
|
167 * A naïve serialization will run into issues if there is a Closure or
|
Chris@13
|
168 * SimpleXMLElement (among other things) in scope when exiting the execution
|
Chris@13
|
169 * loop. We'll just ignore these unserializable classes, and serialize what
|
Chris@13
|
170 * we can.
|
Chris@13
|
171 *
|
Chris@13
|
172 * @param array $return
|
Chris@13
|
173 *
|
Chris@13
|
174 * @return string
|
Chris@13
|
175 */
|
Chris@13
|
176 private function serializeReturn(array $return)
|
Chris@13
|
177 {
|
Chris@13
|
178 $serializable = [];
|
Chris@13
|
179
|
Chris@13
|
180 foreach ($return as $key => $value) {
|
Chris@13
|
181 // No need to return magic variables
|
Chris@13
|
182 if (Context::isSpecialVariableName($key)) {
|
Chris@13
|
183 continue;
|
Chris@13
|
184 }
|
Chris@13
|
185
|
Chris@13
|
186 // Resources and Closures don't error, but they don't serialize well either.
|
Chris@13
|
187 if (is_resource($value) || $value instanceof \Closure) {
|
Chris@13
|
188 continue;
|
Chris@13
|
189 }
|
Chris@13
|
190
|
Chris@13
|
191 try {
|
Chris@13
|
192 @serialize($value);
|
Chris@13
|
193 $serializable[$key] = $value;
|
Chris@13
|
194 } catch (\Throwable $e) {
|
Chris@13
|
195 // we'll just ignore this one...
|
Chris@13
|
196 } catch (\Exception $e) {
|
Chris@13
|
197 // and this one too...
|
Chris@13
|
198 // @todo remove this once we don't support PHP 5.x anymore :)
|
Chris@13
|
199 }
|
Chris@13
|
200 }
|
Chris@13
|
201
|
Chris@13
|
202 return @serialize($serializable);
|
Chris@13
|
203 }
|
Chris@13
|
204 }
|