Chris@13: 0) { Chris@13: // This is the main thread. We'll just wait for a while. Chris@13: Chris@13: // We won't be needing this one. Chris@17: \fclose($up); Chris@13: Chris@13: // Wait for a return value from the loop process. Chris@13: $read = [$down]; Chris@13: $write = null; Chris@13: $except = null; Chris@16: Chris@16: do { Chris@17: $n = @\stream_select($read, $write, $except, null); Chris@16: Chris@16: if ($n === 0) { Chris@16: throw new \RuntimeException('Process timed out waiting for execution loop'); Chris@16: } Chris@16: Chris@16: if ($n === false) { Chris@17: $err = \error_get_last(); Chris@17: if (!isset($err['message']) || \stripos($err['message'], 'interrupted system call') === false) { Chris@16: $msg = $err['message'] ? Chris@17: \sprintf('Error waiting for execution loop: %s', $err['message']) : Chris@16: 'Error waiting for execution loop'; Chris@16: throw new \RuntimeException($msg); Chris@16: } Chris@16: } Chris@16: } while ($n < 1); Chris@13: Chris@17: $content = \stream_get_contents($down); Chris@17: \fclose($down); Chris@13: Chris@13: if ($content) { Chris@17: $shell->setScopeVariables(@\unserialize($content)); Chris@13: } Chris@13: Chris@13: throw new BreakException('Exiting main thread'); Chris@13: } Chris@13: Chris@13: // This is the child process. It's going to do all the work. Chris@17: if (\function_exists('setproctitle')) { Chris@13: setproctitle('psysh (loop)'); Chris@13: } Chris@13: Chris@13: // We won't be needing this one. Chris@17: \fclose($down); Chris@13: Chris@13: // Save this; we'll need to close it in `afterRun` Chris@13: $this->up = $up; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Create a savegame at the start of each loop iteration. Chris@13: * Chris@13: * @param Shell $shell Chris@13: */ Chris@13: public function beforeLoop(Shell $shell) Chris@13: { Chris@13: $this->createSavegame(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Clean up old savegames at the end of each loop iteration. Chris@13: * Chris@13: * @param Shell $shell Chris@13: */ Chris@13: public function afterLoop(Shell $shell) Chris@13: { Chris@13: // if there's an old savegame hanging around, let's kill it. Chris@13: if (isset($this->savegame)) { Chris@17: \posix_kill($this->savegame, SIGKILL); Chris@17: \pcntl_signal_dispatch(); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * After the REPL session ends, send the scope variables back up to the main Chris@13: * thread (if this is a child thread). Chris@13: * Chris@13: * @param Shell $shell Chris@13: */ Chris@13: public function afterRun(Shell $shell) Chris@13: { Chris@13: // We're a child thread. Send the scope variables back up to the main thread. Chris@13: if (isset($this->up)) { Chris@17: \fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false))); Chris@17: \fclose($this->up); Chris@13: Chris@17: \posix_kill(\posix_getpid(), SIGKILL); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Create a savegame fork. Chris@13: * Chris@13: * The savegame contains the current execution state, and can be resumed in Chris@13: * the event that the worker dies unexpectedly (for example, by encountering Chris@13: * a PHP fatal error). Chris@13: */ Chris@13: private function createSavegame() Chris@13: { Chris@13: // the current process will become the savegame Chris@17: $this->savegame = \posix_getpid(); Chris@13: Chris@17: $pid = \pcntl_fork(); Chris@13: if ($pid < 0) { Chris@13: throw new \RuntimeException('Unable to create savegame fork'); Chris@13: } elseif ($pid > 0) { Chris@13: // we're the savegame now... let's wait and see what happens Chris@17: \pcntl_waitpid($pid, $status); Chris@13: Chris@13: // worker exited cleanly, let's bail Chris@17: if (!\pcntl_wexitstatus($status)) { Chris@17: \posix_kill(\posix_getpid(), SIGKILL); Chris@13: } Chris@13: Chris@13: // worker didn't exit cleanly, we'll need to have another go Chris@13: $this->createSavegame(); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Serialize all serializable return values. Chris@13: * Chris@13: * A naïve serialization will run into issues if there is a Closure or Chris@13: * SimpleXMLElement (among other things) in scope when exiting the execution Chris@13: * loop. We'll just ignore these unserializable classes, and serialize what Chris@13: * we can. Chris@13: * Chris@13: * @param array $return Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: private function serializeReturn(array $return) Chris@13: { Chris@13: $serializable = []; Chris@13: Chris@13: foreach ($return as $key => $value) { Chris@13: // No need to return magic variables Chris@13: if (Context::isSpecialVariableName($key)) { Chris@13: continue; Chris@13: } Chris@13: Chris@13: // Resources and Closures don't error, but they don't serialize well either. Chris@17: if (\is_resource($value) || $value instanceof \Closure) { Chris@13: continue; Chris@13: } Chris@13: Chris@13: try { Chris@17: @\serialize($value); Chris@13: $serializable[$key] = $value; Chris@13: } catch (\Throwable $e) { Chris@13: // we'll just ignore this one... Chris@13: } catch (\Exception $e) { Chris@13: // and this one too... Chris@13: // @todo remove this once we don't support PHP 5.x anymore :) Chris@13: } Chris@13: } Chris@13: Chris@17: return @\serialize($serializable); Chris@13: } Chris@13: }