Friday, March 27, 2009

Asterisk FastAGI server in PHP

Here is a new usage of the PEAR Net_Server class. This time a FastAGI server for Asterisk. In the previous example I used the "Sequential" driver, but this time we'll use the "Fork" driver. We are also using the System_Daemon class from PEAR.

First the server script:



require_once 'System/Daemon.php';

System_Daemon::setOption("appName", "FastAGI");
System_Daemon::setOption("authorEmail", "mortena@tpn.no");

System_Daemon::start();

ob_implicit_flush(true);

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

require_once 'Net/Server.php';

require_once 'lib/FastAGI.php';

$server = Net_Server::create('fork', '', );
$server->setEndCharacter("\n\n");
$server->setCallbackObject(new FastAGI());

$server->start();

System_Daemon::stop();

?>


And here is the Net_Server handler class:



require_once 'Net/Server/Handler.php';
require_once 'lib/FastAGI/Command.php';

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/

/**
*
* @author Morten Amundsen
*/
final class FastAGI extends Net_Server_Handler
{
/**
*
* @param integer $clientId
* @param string $data
*/
public function onReceiveData($clientId = 0, $data = '')
{
try {
$cmd = new FastAGI_Command($data, $this->_server);

$retval = $cmd->execute();

$this->convertAndReturn($retval);

$this->setAsteriskVar('fastagi_status', 'OK');

} catch (Exception $e) {

$this->setAsteriskVar('fastagi_error_message', $e->getMessage());
$this->setAsteriskVar('fastagi_status', 'ERROR');
}

$this->_server->closeConnection();
}

/**
*
* @param mixed $retval
*/
protected function convertAndReturn($retval)
{
if (is_array($retval) or is_object($retval)) {
foreach ($retval as $key => $value) {
if (!is_object($value) and !is_array($value)) {
$this->setAsteriskVar($key, $value);
}
}
} else {
$this->setAsteriskVar('return_value', $retval);
}
}

/**
*
* @param string $var Asterisk variable name
* @param string $value Asterisk variable value
*/
protected function setAsteriskVar($var, $value)
{
$this->_server->sendData("SET VARIABLE \"{$var}\" \"{$value}\"");
}
}

/**
* Description of Command
*
* @author Morten Amundsen
*/
class FastAGI_Command
{
protected $class;
protected $method;
protected $params;
protected $channel;
protected $lang;
protected $type;
protected $uniqueid;
protected $callerid;
protected $calleridname;
protected $dnid;
protected $context;
protected $exten;
protected $pri;

protected $connection;

public function __construct($msg, $connection)
{
$lines = explode("\n", $msg);

$this->connection = $connection;

foreach ($lines as $line) {

$parts = explode(':', trim($line));
switch ($parts[0]) {
case 'agi_request':
unset($parts[0]);
$query = implode(':', $parts);

if ($data = parse_url(trim($query))) {
if (!empty($data['query'])) {
parse_str($data['query'], $this->params);
} else {
$this->params = array();
}
$pathparts = explode('/', substr($data['path'], 1, strlen($data['path'])-1));
$cmd = $pathparts[count($pathparts)-1];
unset($pathparts[count($pathparts)-1]);
if (!count($pathparts)) {
$this->class = $cmd;
$this->method = 'default';
} else {
$this->class = implode('_', $pathparts);
$this->method = $cmd;
}

} else {
throw new Exception("Query not understood: {$query}");
}
break;
case 'agi_channel':
$this->channel = $parts[1];
break;
case 'agi_uniqueid':
$this->uniqueid = $parts[1];
break;
case 'agi_callerid':
$this->callerid = $parts[1];
break;
case 'agi_context':
$this->context = $parts[1];
break;
case 'agi_extension':
$this->exten = $parts[1];
break;
case 'agi_priority':
$this->pri = $parts[1];
break;
}
}
}

/**
*
* @return mixed
*/
public function execute()
{
Zend_Loader::loadClass($this->class);

if (class_exists($this->class)) {
$class = $this->class;
$obj = new $class;

if (method_exists($obj, $this->method)) {
return call_user_func_array(array($obj, $this->method), $this->params);
} else {
throw new Exception("Method {$this->method} does not exist in class {$this->class}");
}

} else {
throw new Exception("No such class '{$this->class}'");
}
}
}

?>


It should now be possible to call your FastAGI server from Asterisk like this:


exten => s,1,AGI(agi://[server]:[port]/Foo/Bar/hello?name=Morten


This would autoload the class Foo_Bar, and call the method hello with the parameter name=Morten, and have the result as new variables in your Asterisk call.

PHP CLI XDebug client

This shows how to create a simple XDebug client in PHP.
When the client is started, it waits for a PHP script to start, and then begins to take input
from the user.



The source formatting doesn't look too great, but it's readable, I guess...



We start off with the actual server that listens to port 9000:


#!/usr/bin/php

/*
* Xdebug settings in php.ini
*
* xdebug.remote_autostart=on
* xdebug.remote_enable=on
* xdebug.remote_handler=dbgp
* xdebug.remote_mode=req
* xdebug.remote_host=localhost
* xdebug.remote_port=9000
*/

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

// PEAR
require_once 'Net/Server.php';

require_once 'lib/CLIDebug.php';

$server = &Net_Server::create('sequential', 'localhost', 9000);

$server->setCallbackObject(new CLIDebug());
$server->setEndCharacter("\0");

$server->setDebugMode(false);

$server->start();

?>


Our XDebug client is added via the "setCallbackObject" method, and
looks like this:



/**
* Description of CLIDebug
*
* @author Morten Amundsen
*/
class CLIDebug extends Net_Server_Handler
{
private $_clientid = 0;
private $_appid = 0;
private $_app = '';
private $_status = '';

private $_currline = 0;

/**
*
* @param int $client
* @param string $data
* @return boolean
*/
public function onReceiveData($client = 0, $data = '')
{
$this->_client = $client;

if (is_numeric(trim($data)) or !trim($data)) return true;

$xml = simplexml_load_string($data);

if ($xml) {
if (!empty($xml['appid']) and !empty($xml['fileuri'])) {
$this->_appid = $xml['appid'];
$this->_app = $xml['fileuri'];

$this->_printHelp();
}

$this->_status = (string) $xml['status'];

if ($this->_status == 'stopping') {
$this->_server->closeConnection();
die("Done.\n");
}

$command = (string) $xml['command'];

switch ($command) {
case 'source':
$code = (string) $xml;
$code = base64_decode($code);
echo $code."\n";
break;
case 'step_over':
case 'step_into':
$xdebug = $xml->children('http://xdebug.org/dbgp/xdebug');

$this->_printFileAndLine($xdebug);

if ($this->_cmdSourceLineXdebug($xdebug)) {
return true;
}
default:
}

flush();

} else {
return true;
}

do {
echo ">";

flush();

while (($cmd = $this->waitForCommand()) === false);
} while (!$this->_executeCommand($cmd));
}

/**
*
* @return mixed
*/
public function waitForCommand()
{
if ($fh = fopen("php://STDIN", 'r')) {
$cmd = fgets($fh, 100);
fclose($fh);

return trim($cmd);
} else {
echo "Unable to connect to php://STDIN\n";
return false;
}
}

/**
*
* @return string Command
*/
private function _executeCommand($cmd)
{
$cmd = trim(strtolower($cmd));

switch ($cmd) {
case 'o':
case 'step_over':
return $this->_cmdStepOver();
break;
case 'i':
case 'step_into':
return $this->_cmdStepInto();
break;
case 'h':
case 'help':
$this->_printHelp();
return false;
break;
case 'q':
case 'quit':
$this->_server->closeConnection();
die("Quitting.\n");
default:
echo "Unknown command: ".$cmd."\n";
return false;
}

return true;
}

/**
*
* @return boolean
*/
private function _cmdStepOver()
{
$cmd = "step_over -i {$this->_appid}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @return boolean
*/
private function _cmdStepInto()
{
$cmd = "step_into -i {$this->_appid}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @param SimpleXMLElement $xdebug
* @return boolean
*/
private function _cmdSourceLineXdebug($xdebug)
{
$attr = $xdebug->attributes();

if ($attr and isset($attr['lineno'])) {

$line = (string) $attr['lineno'];
$file = (string) $attr['filename'];

$cmd = "source -i {$this->_appid} -b {$line} -e {$line} -f {$file}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
} else {
return false;
}
}

/**
*
* @param SimpleXMLElement $xdebug
* @return boolean
*/
private function _cmdSourceLines($file, $begin, $end)
{
$cmd = "source -i {$this->_appid} -b {$begin} -e {$end} -f {$file}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @param SimpleXMLElement $xdebug
*/
private function _printFileAndLine($xdebug)
{
$attr = $xdebug->attributes();

$this->_app = (string) $attr['filename'];

$line = (string) $attr['lineno'];
$this->_currline = $line;

echo "File: ".(string) $this->_app . " - Line: " . (string) $attr['lineno']." (".$this->_status.")\n";
}

/**
*
*/
private function _printHelp()
{
echo "i|step_into\n\tsteps to the next statement, if there is a function call\n\tinvolved it will break on the first statement in that function\n";
echo "o|step_over\n\tsteps to the next statement, if there is a function call\n\ton the line from which the step_over is issued then the debugger engine will stop at the\n\tstatement after the function call in the same\n\tscope as from where the command was issued\n";
echo "h|help\n\tThis help text\n";
echo "q|quit\n\tQuit\n";
flush();
}
}
?>


Now it's only a matter of filling in the blanks with the rest of the DBGP protocol.

Thursday, March 19, 2009

Heaven

Posted via Pixelpipe.

In Rome by Colloseum

Posted via Pixelpipe.

PHP italia

Posted via Pixelpipe.

Wednesday, March 18, 2009

In Rome for dinner #phpitalia

Posted via Pixelpipe.

Saturday, March 07, 2009

Beautiful day on the Riviera

Posted via Pixelpipe.

Friday, March 06, 2009

2006 Foradori Teroldego Rotaliano

Posted via Pixelpipe.

I saw Gary Vaynerchuk try this on episode 636, and I will definitly drink this wine again...

Tuesday, March 03, 2009

The Fog of Romagna

Posted via Pixelpipe.

Monday, March 02, 2009

Can't wait for PHPCon Italia 2009.

Posted via Pixelpipe.