You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
6.1 KiB
PHP

<?php
/* vim: set shiftwidth=2 expandtab softtabstop=2: */
namespace Boris;
/**
* EvalWorker is responsible for evaluating PHP expressions in forked processes.
*/
class EvalWorker {
const ABNORMAL_EXIT = 255;
const DONE = "\0";
const EXITED = "\1";
const FAILED = "\2";
const READY = "\3";
private $_socket;
private $_exports = array();
private $_startHooks = array();
private $_failureHooks = array();
private $_ppid;
private $_pid;
private $_cancelled;
private $_inspector;
private $_exceptionHandler;
/**
* Create a new worker using the given socket for communication.
*
* @param resource $socket
*/
public function __construct($socket) {
$this->_socket = $socket;
$this->_inspector = new DumpInspector();
stream_set_blocking($socket, 0);
}
/**
* Set local variables to be placed in the workers's scope.
*
* @param array|string $local
* @param mixed $value, if $local is a string
*/
public function setLocal($local, $value = null) {
if (!is_array($local)) {
$local = array($local => $value);
}
$this->_exports = array_merge($this->_exports, $local);
}
/**
* Set hooks to run inside the worker before it starts looping.
*
* @param array $hooks
*/
public function setStartHooks($hooks) {
$this->_startHooks = $hooks;
}
/**
* Set hooks to run inside the worker after a fatal error is caught.
*
* @param array $hooks
*/
public function setFailureHooks($hooks) {
$this->_failureHooks = $hooks;
}
/**
* Set an Inspector object for Boris to output return values with.
*
* @param object $inspector any object the responds to inspect($v)
*/
public function setInspector($inspector) {
$this->_inspector = $inspector;
}
/**
* Start the worker.
*
* This method never returns.
*/
public function start() {
$__scope = $this->_runHooks($this->_startHooks);
extract($__scope);
$this->_write($this->_socket, self::READY);
/* Note the naming of the local variables due to shared scope with the user here */
for (;;) {
declare(ticks = 1);
// don't exit on ctrl-c
pcntl_signal(SIGINT, SIG_IGN, true);
$this->_cancelled = false;
$__input = $this->_transform($this->_read($this->_socket));
if ($__input === null) {
continue;
}
$__response = self::DONE;
$this->_ppid = posix_getpid();
$this->_pid = pcntl_fork();
if ($this->_pid < 0) {
throw new \RuntimeException('Failed to fork child labourer');
} elseif ($this->_pid > 0) {
// kill the child on ctrl-c
pcntl_signal(SIGINT, array($this, 'cancelOperation'), true);
pcntl_waitpid($this->_pid, $__status);
if (!$this->_cancelled && $__status != (self::ABNORMAL_EXIT << 8)) {
$__response = self::EXITED;
} else {
$this->_runHooks($this->_failureHooks);
$__response = self::FAILED;
}
} else {
// user exception handlers normally cause a clean exit, so Boris will exit too
if (!$this->_exceptionHandler =
set_exception_handler(array($this, 'delegateExceptionHandler'))) {
restore_exception_handler();
}
// undo ctrl-c signal handling ready for user code execution
pcntl_signal(SIGINT, SIG_DFL, true);
$__pid = posix_getpid();
$__result = eval($__input);
if (posix_getpid() != $__pid) {
// whatever the user entered caused a forked child
// (totally valid, but we don't want that child to loop and wait for input)
exit(0);
}
if (preg_match('/\s*return\b/i', $__input)) {
fwrite(STDOUT, sprintf("%s\n", $this->_inspector->inspect($__result)));
}
$this->_expungeOldWorker();
}
$this->_write($this->_socket, $__response);
if ($__response == self::EXITED) {
exit(0);
}
}
}
/**
* While a child process is running, terminate it immediately.
*/
public function cancelOperation() {
printf("Cancelling...\n");
$this->_cancelled = true;
posix_kill($this->_pid, SIGKILL);
pcntl_signal_dispatch();
}
/**
* If any user-defined exception handler is present, call it, but be sure to exit correctly.
*/
public function delegateExceptionHandler($ex) {
call_user_func($this->_exceptionHandler, $ex);
exit(self::ABNORMAL_EXIT);
}
// -- Private Methods
private function _runHooks($hooks) {
extract($this->_exports);
foreach ($hooks as $__hook) {
if (is_string($__hook)) {
eval($__hook);
} elseif (is_callable($__hook)) {
call_user_func($__hook, $this, get_defined_vars());
} else {
throw new \RuntimeException(
sprintf(
'Hooks must be closures or strings of PHP code. Got [%s].',
gettype($__hook)
)
);
}
// hooks may set locals
extract($this->_exports);
}
return get_defined_vars();
}
private function _expungeOldWorker() {
posix_kill($this->_ppid, SIGTERM);
pcntl_signal_dispatch();
}
private function _write($socket, $data) {
if (!fwrite($socket, $data)) {
throw new \RuntimeException('Socket error: failed to write data');
}
}
private function _read($socket)
{
$read = array($socket);
$except = array($socket);
if ($this->_select($read, $except) > 0) {
if ($read) {
return stream_get_contents($read[0]);
} else if ($except) {
throw new \UnexpectedValueException("Socket error: closed");
}
}
}
private function _select(&$read, &$except) {
$write = null;
set_error_handler(function(){return true;}, E_WARNING);
$result = stream_select($read, $write, $except, 10);
restore_error_handler();
return $result;
}
private function _transform($input) {
if ($input === null) {
return null;
}
$transforms = array(
'exit' => 'exit(0)'
);
foreach ($transforms as $from => $to) {
$input = preg_replace('/^\s*' . preg_quote($from, '/') . '\s*;?\s*$/', $to . ';', $input);
}
return $input;
}
}