lib/php_process/php_script.php
#!/usr/bin/env php5
<?php
//Controls the PHP-process on the PHP-side.
class php_process{
private $errortypes = array (
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parsing Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Runtime Notice'
);
//Opens stdin and stdout for processing. Sets various helper-variables.
function __construct(){
$this->sock_stdin = fopen("php://stdin", "r");
$this->sock_stdout = fopen("php://stdout", "w");
$this->objects = array();
$this->objects_spl = array();
$this->objects_count = 0;
$this->created_functions = array();
$this->proxy_to_func = array("call_created_func", "constant_val", "create_func", "func", "get_var", "memory_info", "object_cache_info", "object_call", "require_once_path", "set_var", "static_method_call", "unset_ids");
$this->func_specials = array("constant", "define", "die", "echo", "exit", "require", "require_once", "include", "include_once");
$this->send_count = 0;
print "php_script_ready:" . getmypid() . "\n";
}
//Starts listening in stdin for new instructions. Calls 'handle_line' for every line gotten.
function start_listening(){
while(true){
$line = fgets($this->sock_stdin, 1048576);
$this->handle_line($line);
}
}
//Writes the given data to stdout. Serializes and encodes it as well and increases the 'send_count'-variable.
function send($data){
$id = $this->send_count;
$this->send_count++;
$data_packed = trim(base64_encode(serialize($data)));
if (!fwrite($this->sock_stdout, "%{{php_process:begin}}send:" . $id . ":" . $data_packed . "%{{php_process:end}}\n")){
throw new exception("Could not write to stdout.");
}
}
//Handles the given instruction. It parses it and then calls the relevant method.
function handle_line($line){
$data = explode(":", $line);
$type = $data[0];
$id = intval($data[1]);
$args = unserialize(base64_decode($data[2]));
if ($args === false){
throw new exception("The args-data was not unserializeable: " . base64_decode($data[2]));
}
try{
if ($type == "send"){
if ($args["type"] == "eval"){
$res = eval($args["eval_str"] . ";");
$this->answer($id, $res);
}elseif($args["type"] == "new"){
$this->new_object($id, $args);
}elseif(in_array($args["type"], $this->proxy_to_func)){
$this->$args["type"]($id, $args);
}else{
throw new exception("Unknown send-type: " . $args["type"] . " (" . implode(", ", array_keys($args)) . ") (" . base64_decode($data[2]) . ")");
}
}else{
throw new exception("Invalid type: " . $type);
}
}catch(php_process_native_ruby_exception $e){
$this->answer($id, array("type" => "exception", "msg" => $e->getMessage(), "bt" => $e->getTraceAsString(), "ruby_type" => $e->ruby_exc_name));
}catch(exception $e){
$this->answer($id, array("type" => "exception", "msg" => $e->getMessage(), "bt" => $e->getTraceAsString()));
}
}
//Parses objects into special arrays, which again will be turned into proxy-objects on the Ruby-side. Recursivly scans arrays to do the same.
function parse_data($data){
if (is_array($data)){
foreach($data as $key => $val){
if (is_object($val)){
$data[$key] = $this->parse_data($val);
}
}
return $data;
}elseif(is_object($data)){
$spl = spl_object_hash($data);
if (array_key_exists($spl, $this->objects_spl)){
$id = $this->objects_spl[$spl];
}else{
$id = $this->objects_count;
$this->objects_count++;
if (array_key_exists($id, $this->objects)){
throw new exception("Object with that ID already exists: " . $id);
}
$this->objects[$id] = array("obj" => $data, "spl" => $spl);
$this->objects_spl[$spl] = $id;
}
$ret = array("proxyobj", $id);
return $ret;
}else{
return $data;
}
}
//Recursivly read the given data from the Ruby-side. Changing special arrays into the objects they refer to.
function read_parsed_data($data){
if (is_array($data) and array_key_exists("type", $data) and $data["type"] == "proxyobj" and array_key_exists("id", $data)){
$object = $this->objects[$data["id"]]["obj"];
if (!$object){
throw new exception("No object by that ID: " . $data["id"]);
}
return $object;
}elseif(is_array($data) and array_key_exists("type", $data) and $data["type"] == "php_process_created_function" and array_key_exists("id", $data)){
$func = $this->created_functions[$data["id"]]["func"];
return $func;
}elseif(is_array($data)){
foreach($data as $key => $val){
$data[$key] = $this->read_parsed_data($val);
}
return $data;
}else{
return $data;
}
}
//Answers a request ID with the given data. Writes it to stdout.
function answer($id, $data){
if (!fwrite($this->sock_stdout, "%{{php_process:begin}}answer:" . $id . ":" . base64_encode(serialize($this->parse_data($data))) . "%{{php_process:end}}\n")){
throw new exception("Could not write to socket.");
}
}
//Ruby wants spawn an object. Cache it and return (Ruby will tell us when to unset it automatically).
function new_object($id, $args){
$class = $args["class"];
$new_args = $this->read_parsed_data($args["args"]);
$klass = new ReflectionClass($class);
$object = $klass->newInstanceArgs($new_args);
$this->answer($id, $object);
}
//Ruby wants to set an instance variable on an object. Do that and answer with 'true'.
function set_var($id, $args){
$object = $this->objects[$args["id"]]["obj"];
if (!$object){
throw new exception("No object by that ID: " . $args["id"]);
}
$object->$args["name"] = $args["val"];
$this->answer($id, true);
}
//Ruby wants to read an instance variable on an object. Return the variable's value.
function get_var($id, $args){
$object = $this->objects[$args["id"]]["obj"];
if (!$object){
throw new exception("No object by that ID: " . $args["id"]);
}
$this->answer($id, $object->$args["name"]);
}
//Ruby wants to call a method on an object. Do that and return the result.
function object_call($id, $args){
if (!array_key_exists($args["id"], $this->objects)){
throw new exception("No object by that ID: " . $args["id"]);
}
$object = $this->objects[$args["id"]]["obj"];
$call_arr = array($object, $args["method"]);
//Error handeling.
if (!$object){
throw new exception("No object by that ID: " . $args["id"]);
}elseif(!method_exists($object, $args["method"])){
throw new exception("No such method: " . get_class($object) . "->" . $args["method"] . "()");
}elseif(!is_callable($call_arr)){
throw new exception("Not callable: " . get_class($object) . "->" . $args["method"] . "()");
}
$res = call_user_func_array($call_arr, $this->read_parsed_data($args["args"]));
$this->answer($id, $res);
}
//Ruby wants to call a function. Do that and return the result.
function func($id, $args){
//These functions cant be called normally. Hack them with eval instead.
$newargs = $this->read_parsed_data($args["args"]);
if (in_array($args["func_name"], $this->func_specials)){
$eval_str = $args["func_name"] . "(";
$count = 0;
foreach($newargs as $key => $val){
if (!is_numeric($key)){
throw new exception("Invalid key: '" . $key . "'.");
}
if ($count > 0){
$eval_str .= ",";
}
$eval_str .= "\$args['args'][" . $count . "]";
$count++;
}
$eval_str .= ");";
$res = eval($eval_str);
}else{
if (!function_exists($args["func_name"])){
throw new php_process_native_ruby_exception("Method does not exist: '" . $args["func_name"] . "'.", "NoMethodError");
}
$res = call_user_func_array($args["func_name"], $newargs);
}
$this->answer($id, $res);
}
//Ruby has given us various object-IDs to unset. Unset them from cache and return 'true'.
function unset_ids($id, $args){
foreach($args["ids"] as $obj_id){
if (!array_key_exists($obj_id, $this->objects)){
continue;
}
$spl = $this->objects[$obj_id]["spl"];
if (!array_key_exists($spl, $this->objects_spl)){
throw new exception("SPL could not be found: " . $spl);
}
unset($this->objects_spl[$spl]);
unset($this->objects[$obj_id]);
}
$this->answer($id, true);
}
//Ruby wants information about the object-cache on the PHP-side. Return that in an array.
function object_cache_info($id, $args){
$types = array();
foreach($this->objects as $key => $val){
if (is_object($val)){
$types[] = "object: " . get_class($val);
}else{
$types[] = gettype($val);
}
}
$this->answer($id, array(
"count" => count($this->objects),
"types" => $types
));
}
//Ruby wants to call a static method. Answers with the result.
function static_method_call($id, $args){
$call_arr = array($args["class_name"], $args["method_name"]);
if (!class_exists($args["class_name"])){
throw new php_process_native_ruby_exception("Class does not exist: '" . $args["class_name"] . "'.", "NameError");
}elseif(!method_exists($args["class_name"], $args["method_name"])){
throw new php_process_native_ruby_exception("Such a static method does not exist: " . $args["class_name"] . "::" . $args["method_name"] . "()", "NoMethodError");
}elseif(!is_callable($call_arr)){
throw new exception("Invalid class-name (" . $args["class_name"] . ") or method-name (" . $args["method_name"] . "). It was not callable.");
}
$newargs = $this->read_parsed_data($args["args"]);
$res = call_user_func_array($call_arr, $newargs);
$this->answer($id, $res);
}
//Creates a function which can be used for callbacks on the Ruby-side.
function create_func($id, $args){
$cb_id = $args["callback_id"];
$func = create_function("", "global \$php_process; \$php_process->call_back_created_func(" . $cb_id . ", func_get_args());");
if (!$func){
throw new exception("Could not create function.");
}
$this->created_functions[$cb_id] = array("func" => $func);
$this->answer($id, true);
}
//This function is called, when a create-function is called. It then callbacks to Ruby, where a 'Proc' will be executed.
function call_back_created_func($func_id, $args){
$this->send(array(
"type" => "call_back_created_func",
"func_id" => $func_id,
"args" => $args
));
}
//Ruby wants to call a created function. Pretty much just executes that function. This is useually done for debugging callbacks.
function call_created_func($id, $args){
$func = $this->created_functions[$args["id"]]["func"];
if (!$func){
throw new exception("No created function by that ID: '" . $args["id"] . "'.\n\n" . print_r($args, true) . "\n\n" . print_r($this->created_functions, true));
}
$eval_str = "\$func(";
$count = 0;
foreach($args["args"] as $key => $val){
if ($count > 0){
$eval_str .= ", ";
}
$eval_str .= "\$args['args'][" . $count . "]";
$count++;
}
$eval_str .= ");";
$res = eval($eval_str);
$this->answer($id, $res);
}
//Ruby wants to read a constant. This is not done by 'func', because of keeping caching posibility open and not wanting to eval.
function constant_val($id, $args){
$this->answer($id, constant($args["name"]));
}
//Returns various information about the object-cache.
function memory_info($id, $args){
$this->answer($id, array(
"objects" => count($this->objects),
"objects_spl" => count($this->objects_spl),
"created_functions" => count($this->created_functions)
));
}
//Makes errors being thrown as exceptions instead.
function error_handler($error_number, $error_message, $file_name, $line_number, $vars, $args = null){
if ($error_number == E_STRICT || $error_number == E_NOTICE || $error_number == E_WARNING){
$this->send(array(
"type" => "php_error",
"error_message" => $error_message,
"error_type" => $this->errortypes[$error_number],
"file_name" => $file_name,
"line_number" => $line_number,
"backtrace" => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
));
return null;
}
throw new exception("Error " . $this->errortypes[$error_number] . ": " . $error_message . " in \"" . $file_name . ":" . $line_number);
}
}
class php_process_native_ruby_exception extends exception{
public $msg;
public $ruby_exc_name;
function __construct($msg, $ruby_exc_name){
parent::__construct($msg);
$this->ruby_exc_name = $ruby_exc_name;
}
}
//Spawn the main object.
$php_process = new php_process();
//Set error-level and make warnings and errors being thrown as exceptions.
set_error_handler(array($php_process, "error_handler"));
error_reporting(E_ALL ^ E_WARNING ^ E_NOTICE ^ E_STRICT);
//Start listening for instructions from host process.
$php_process->start_listening();