annotate 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
rev   line source
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 }