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