Improved the external module - supports connecting on sockets, watching

message answers and option settings.


git-svn-id: http://voip.null.ro/svn/yate@729 acf43c95-373e-0410-b603-e72c3f656dc1
This commit is contained in:
paulc 2006-03-29 23:05:36 +00:00
parent 984697273a
commit 589b63a931
15 changed files with 933 additions and 163 deletions

View File

@ -1,3 +1,10 @@
Thu Mar 30 2006 Paul Chitescu <paulc-devel@null.ro>
- Changed external module to allow to listen on sockets
- Added the watch concept designed by Maciek Kaminski
- Added many options that can be set per external module instance
- Updated the PHP libraries to support the new functions
- Changed the return values of GetEvent() to comply with typechecks in PHP5
Wed Mar 29 2006 Paul Chitescu <paulc-devel@null.ro>
- Modified conference based on Andrew McDonald's idea of N-way mixing
- Default output in rmanager is controlled by the config file

View File

@ -7,6 +7,28 @@
; Uncomment the following line when running in the sources directory
;scripts_dir=scripts/
; timeout: int: How many milliseconds to wait for a module to answer
;timeout=10000
; timebomb: bool: Kill the module instance if it timed out
;timebomb=false
;[listener sample]
; For each socket listener there should be a section starting with the
; "listener" keyword
; type: keyword: Type of socket - "unix" or "tcp"
; path: string: Path of the UNIX socket to create
; addr: string: IP address to bind the TCP socket to
;addr=127.0.0.1
; port: int: TCP port to bind to, must be positive
; role: keyword: Role of incoming connections - "global", "channel" or don't set
[scripts]
; Add one entry in this section for each global external module that is to be
@ -17,3 +39,13 @@
; and name relative to the sripts_dir in section [general]
; The parameter is optional and if present is passed to the script as the first
; (and single) parameter
[execute]
; Add one entry in this section for each external program that is to be
; executed on Yate startup
; Each line has to be on the form:
; progname=parameter
; The program name should hold the absolute path to the program
; The parameter is optional and if present is passed to the program as the first
; (and single) parameter

View File

@ -19,6 +19,14 @@ several file descriptors:<br />
File descriptors 3 and 4 are open only for audio capable applications.<br />
<h2>Socket operation</h2>
Yate can start one or more socket listeners and wait for external programs to
connect to them. Depending on the platform, TCP and UNIX sockets may be available.<br />
Once connected, an external program uses this single socket to send commands and
receive answers from the engine.<br />
For reporting errors it is recommended to use the <b>%%&gt;output</b> keyword.<br />
Each listener can have a single role or the connecting program will have to
use the <b>%%&gt;connect</b> command first to esablish a role.<br />
<h2>Format of commands and notifications</h2>
@ -127,7 +135,7 @@ properly or not.<br />
</p>
<p><b>Keyword: %%&gt;uninstall</b><br />
%%&gt;install:&lt;name&gt;<br />
%%&gt;uninstall:&lt;name&gt;<br />
<b>Direction: Application to engine</b><br />
Always from the application to the engine, requests uninstalling a previously
installed message handler<br />
@ -136,7 +144,7 @@ The answer to the uninstall request is delivered asynchronously (see below).<br
</p>
<p><b>Keyword: %%&lt;uninstall</b><br />
%%&lt;install:&lt;priority&gt;:&lt;name&gt;:&lt;success&gt;<br />
%%&lt;uninstall:&lt;priority&gt;:&lt;name&gt;:&lt;success&gt;<br />
<b>Direction: Engine to application</b><br />
Confirmation from engine to the application that the handler has been uninstalled
properly or not.<br />
@ -145,6 +153,42 @@ properly or not.<br />
&lt;success&gt; - boolean (&quot;true&quot; or &quot;false&quot;) success of operation<br />
</p>
<p><b>Keyword: %%&gt;watch</b><br />
%%&gt;watch:&lt;name&gt;<br />
<b>Direction: Application to engine</b><br />
Always from the application to the engine, requests the installing of a message
watcher (post-dispatching notifier) for the application that requested it<br />
The answer to the watch request is delivered asynchronously (see below).<br />
&lt;name&gt; - name of the messages for that a watcher should be installed<br />
</p>
<p><b>Keyword: %%&lt;watch</b><br />
%%&lt;watch:&lt;name&gt;:&lt;success&gt;<br />
<b>Direction: Engine to application</b><br />
Confirmation from engine to the application that the watcher has been installed
properly or not.<br />
&lt;name&gt; - name of the messages asked to watch<br />
&lt;success&gt; - boolean (&quot;true&quot; or &quot;false&quot;) success of operation<br />
</p>
<p><b>Keyword: %%&gt;unwatch</b><br />
%%&gt;unwatch:&lt;name&gt;<br />
<b>Direction: Application to engine</b><br />
Always from the application to the engine, requests uninstalling a previously
installed message watcher<br />
The answer to the unwatch request is delivered asynchronously (see below).<br />
&lt;name&gt; - name of the message watcher thst should be uninstalled<br />
</p>
<p><b>Keyword: %%&lt;unwatch</b><br />
%%&lt;unwatch:&lt;name&gt;:&lt;success&gt;<br />
<b>Direction: Engine to application</b><br />
Confirmation from engine to the application that the watcher has been uninstalled
properly or not.<br />
&lt;name&gt; - name of the message watcher asked to uninstall<br />
&lt;success&gt; - boolean (&quot;true&quot; or &quot;false&quot;) success of operation<br />
</p>
<p><b>Keyword: %%&gt;setlocal</b><br />
%%&gt;setlocal:&lt;name&gt;:&lt;value&gt;<br />
<b>Direction: Application to engine</b><br />
@ -152,7 +196,14 @@ Always from the application to the engine, requests the change of a local
parameter<br />
The answer to the change request is delivered asynchronously (see below).<br />
&lt;name&gt; - name of the parameter to modify, must not be empty<br />
&lt;value&gt; - new value to set in the local module instance<br />
&lt;value&gt; - new value to set in the local module instance, empty to just query<br />
<b>Currently supported parameters:</b><br />
id (string) - Identifier of the associated channel, if any<br />
disconnected (bool) - Enable or disable sending &quot;chan.disconnected&quot; messages<br />
timeout (int) - Timeout in milliseconds for answering to messages<br />
timebomb (bool) - Terminate this module instance if a timeout occured<br />
reenter (bool) - If this module is allowed to handle messages generated by itself<br />
selfwatch (bool) - If this module is allowed to watch messages generated by itself<br />
</p>
<p><b>Keyword: %%&lt;setlocal</b><br />
@ -165,6 +216,15 @@ changed successfully or not.<br />
&lt;success&gt; - boolean (&quot;true&quot; or &quot;false&quot;) success of operation<br />
</p>
<p><b>Keyword: %%&gt;output</b><br />
%%&gt;output:arbitrary unescaped string<br />
<b>Direction: Application to engine</b><br />
The &quot;output&quot; keyword is used to relay arbitrary output messages to
engine's logging output.<br />
This is the proper way of logging messages for programs that connect to the
socket interface as they may not have the standard error redirected.<br />
</p>
<p><b>Keyword: %%&gt;connect</b><br />
%%&gt;connect:&lt;role&gt;[:&lt;id&gt;][:&lt;type&gt;]<br />
<b>Direction: Application to engine</b><br />

View File

@ -282,6 +282,16 @@ Stream::~Stream()
{
}
bool Stream::canRetry() const
{
return false;
}
bool Stream::setBlocking(bool block)
{
return false;
}
int Stream::writeData(const char* str)
{
if (null(str))
@ -422,6 +432,37 @@ void File::copyError()
#endif
}
bool File::canRetry() const
{
if (!m_error)
return true;
return (m_error == EAGAIN) || (m_error == EINTR) || (m_error == EWOULDBLOCK);
}
bool File::setBlocking(bool block)
{
#ifdef _WINDOWS
return false;
#else
unsigned long flags = 1;
flags = ::fcntl(m_handle,F_GETFL);
if (flags < 0) {
copyError();
return false;
}
if (block)
flags &= !O_NONBLOCK;
else
flags |= O_NONBLOCK;
if (::fcntl(m_handle,F_SETFL,flags) < 0) {
copyError();
return false;
}
clearError();
return true;
#endif
}
int File::writeData(const void* buffer, int length)
{
if (!buffer)

File diff suppressed because it is too large Load Diff

View File

@ -129,10 +129,10 @@ function gotNotify()
while ($state != "") {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* No need to handle empty events in this application */
if ($ev == "")
if ($ev === true)
continue;
/* If we reached here we should have a valid object */
switch ($ev->type) {
@ -158,7 +158,7 @@ while ($state != "") {
setState("novmail");
// we already ACKed this message
$ev = "";
$ev = false;
break;
case "chan.notify":
@ -170,7 +170,7 @@ while ($state != "") {
}
/* This is extremely important.
We MUST let messages return, handled or not */
if ($ev != "")
if ($ev)
$ev->Acknowledge();
break;
case "answer":

View File

@ -56,8 +56,11 @@ class Yate
*/
static function Output($str)
{
global $yate_stderr;
fputs($yate_stderr, $str . "\n");
global $yate_stderr, $yate_socket;
if ($yate_socket)
_yate_print("%%>output:$str\n");
else
fputs($yate_stderr, "$str\n");
}
/**
@ -73,7 +76,18 @@ class Yate
else if ($str === false)
$yate_debug = false;
else if ($yate_debug)
fputs($yate_stderr, $str . "\n");
Yate::Output($str);
}
/**
* Static function to get the unique argument passed by Yate at start time
* @return First (and only) command line argument passed by Yate
*/
static function Arg()
{
if (isset($_SERVER['argv'][1]))
return $_SERVER['argv'][1];
return null;
}
/**
@ -154,7 +168,7 @@ class Yate
$name=Yate::Escape($name);
if ($filtname)
$filtname=":$filtname:$filtvalue";
print "%%>install:$priority:$name$filtname\n";
_yate_print("%%>install:$priority:$name$filtname\n");
}
/**
@ -164,7 +178,27 @@ class Yate
static function Uninstall($name)
{
$name=Yate::Escape($name);
print "%%>uninstall:$name\n";
_yate_print("%%>uninstall:$name\n");
}
/**
* Install a Yate message watcher
* @param $name Name of the messages to watch
*/
static function Watch($name)
{
$name=Yate::Escape($name);
_yate_print("%%>watch:$name\n");
}
/**
* Uninstall a Yate message watcher
* @param $name Name of the messages to stop watching
*/
static function Unwatch($name)
{
$name=Yate::Escape($name);
_yate_print("%%>unwatch:$name\n");
}
/**
@ -176,7 +210,7 @@ class Yate
{
$name=Yate::Escape($name);
$value=Yate::Escape($value);
print "%%>setlocal:$name:$value\n";
_yate_print("%%>setlocal:$name:$value\n");
}
/**
@ -211,6 +245,18 @@ class Yate
return $defvalue;
}
/**
* Set a named parameter
* @param $key Name of the parameter to set
* @param $value Value to set in the parameter
*/
function SetParam($key, $value)
{
if (($value === true) || ($value === false))
$value = Yate::Bool2str($value);
$this->params[$key] = $value;
}
/**
* Fill the parameter array from a text representation
* @param $parts A numerically indexed array with the key=value parameters
@ -246,7 +292,7 @@ class Yate
$p="";
$pa = array(&$p);
array_walk($this->params, "_yate_message_walk", $pa);
print "%%>message:$i:$t:$n:$r$p\n";
_yate_print("%%>message:$i:$t:$n:$r$p\n");
$this->type="dispatched";
}
@ -267,28 +313,44 @@ class Yate
$p="";
$pa = array(&$p);
array_walk($this->params, "_yate_message_walk", $pa);
print "%%<message:$i:$k:$n:$r$p\n";
_yate_print("%%<message:$i:$k:$n:$r$p\n");
$this->type="acknowledged";
}
/**
* This static function processes just one input line.
* It must be called in a loop to keep messages running. Or else.
* @return "EOF" if we should exit, "" if we should keep running,
* or an Yate object instance
* @return false if we should exit, true if we should keep running,
* or an Yate object instance. Remember to use === and !== operators
* when comparing against true and false.
*/
static function GetEvent()
{
global $yate_stdin;
if (feof($yate_stdin))
return "EOF";
$line=fgets($yate_stdin,4096);
if ($line == false)
return "";
global $yate_stdin, $yate_socket;
if ($yate_socket) {
$line = @socket_read($yate_socket,8192);
// check for error
if ($line == false)
return false;
// check for EOF
if ($line === "")
return false;
}
else {
if ($yate_stdin == false)
return false;
// check for EOF
if (feof($yate_stdin))
return false;
$line=fgets($yate_stdin,8192);
// check for async read no data
if ($line == false)
return true;
}
$line=str_replace("\n", "", $line);
if ($line == "")
return "";
$ev="";
return true;
$ev=true;
$part=explode(":", $line);
switch ($part[0]) {
case "%%>message":
@ -317,6 +379,24 @@ class Yate
$ev->type="uninstalled";
$ev->handled=Yate::Str2bool($part[3]);
break;
case "%%<watch":
/* watch answer str_name:bool_success */
$ev=new Yate(Yate::Unescape($part[1]));
$ev->type="watched";
$ev->handled=Yate::Str2bool($part[2]);
break;
case "%%<unwatch":
/* unwatch answer str_name:bool_success */
$ev=new Yate(Yate::Unescape($part[1]));
$ev->type="unwatched";
$ev->handled=Yate::Str2bool($part[2]);
break;
case "%%<connect":
/* connect answer str_role:bool_success */
$ev=new Yate(Yate::Unescape($part[1]));
$ev->type="connected";
$ev->handled=Yate::Str2bool($part[2]);
break;
case "%%<setlocal":
/* local parameter answer str_name:str_value:bool_success */
$ev=new Yate(Yate::Unescape($part[1]),Yate::Unescape($part[2]));
@ -336,19 +416,50 @@ class Yate
* This static function initializes globals in the PHP Yate External Module.
* It should be called before any other method.
* @param $async (optional) True if asynchronous, polled mode is desired
* @param $addr Hostname to connect to or UNIX socket path
* @param $port TCP port to connect to, zero to use UNIX sockets
* @param $role Role of this connection - "global" or "channel"
* @return True if initialization succeeded, false if failed
*/
static function Init($async = false)
static function Init($async = false, $addr = "", $port = 0, $role = "")
{
global $yate_stdin, $yate_stdout, $yate_stderr, $yate_debug;
global $yate_stdin, $yate_stdout, $yate_stderr, $yate_socket, $yate_debug;
$yate_debug = false;
$yate_stdin = fopen("php://stdin","r");
$yate_stdout = fopen("php://stdout","w");
$yate_stderr = fopen("php://stderr","w");
$yate_stdin = false;
$yate_stdout = false;
$yate_stderr = false;
if ($addr) {
$ok = false;
if ($port) {
$yate_socket = @socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
$ok = @socket_connect($yate_socket,$addr,$port);
}
else {
$yate_socket = @socket_create(AF_UNIX,SOCK_STREAM,0);
$ok = @socket_connect($yate_socket,$addr,$port);
}
if (($yate_socket === false) || !$ok) {
$yate_socket = false;
$yate_stderr = fopen("php://stderr","w");
Yate::Output("Socket error, initialization failed");
return false;
}
}
else {
$yate_socket = false;
$yate_stdin = fopen("php://stdin","r");
$yate_stdout = fopen("php://stdout","w");
$yate_stderr = fopen("php://stderr","w");
$role = "";
}
flush();
set_error_handler("_yate_error_handler");
ob_implicit_flush(1);
if ($async && function_exists("stream_set_blocking"))
if ($async && function_exists("stream_set_blocking") && $yate_stdin)
stream_set_blocking($yate_stdin,false);
if ($role)
_yate_print("%%>connect:$role\n");
return true;
}
}
@ -359,22 +470,32 @@ function _yate_error_handler($errno, $errstr, $errfile, $errline)
$str = " [$errno] $errstr in $errfile line $errline\n";
switch ($errno) {
case E_USER_ERROR:
Yate::Output("PHP fatal:" . $str);
Yate::Output("PHP fatal: $str");
exit(1);
break;
case E_WARNING:
case E_USER_WARNING:
Yate::Output("PHP error:" . $str);
Yate::Output("PHP error: $str");
break;
case E_NOTICE:
case E_USER_NOTICE:
Yate::Output("PHP warning:" . $str);
Yate::Output("PHP warning: $str");
break;
default:
Yate::Output("PHP unknown error:" . $str);
Yate::Output("PHP unknown error: $str");
}
}
/* Internal function */
function _yate_print($str)
{
global $yate_stdout, $yate_socket;
if ($yate_socket)
@socket_write($yate_socket, $str);
else if ($yate_stdout)
fputs($yate_stdout, $str);
}
/* Internal function */
function _yate_message_walk($item, $key, &$result)
{

View File

@ -69,9 +69,9 @@ class YateChan
$loop = true;
while ($loop) {
$ev=Yate::GetEvent();
if ($ev == "")
if ($ev === true)
continue;
if ($ev == "EOF") {
if ($ev === false) {
$chan_instance->exiting = true;
break;
}
@ -208,11 +208,13 @@ class YateChan
* It will call Yate::Init internally.
* @param $prefix (optional) Prefix used for the unique channel identifier
* @param $async (optional) True if asynchronous, polled mode is desired
* @param $addr Hostname to connect to or UNIX socket path
* @param $port TCP port to connect to, zero to use UNIX sockets
*/
static function Init($prefix = "extchan", $async = false)
static function Init($prefix = "extchan", $async = false, $addr = "", $port = 0)
{
global $chan_instance;
Yate::Init($async);
Yate::Init($async,$addr,$port,"channel");
$chan_instance = new YateChan($prefix);
YateChan::RunEvents();
if ($chan_instance->exiting)

View File

@ -163,9 +163,9 @@ function endRoute($callto,$ok,$err)
while ($state != "") {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
if ($ev == "")
if ($ev === true)
continue;
/* If we reached here we should have a valid object */
switch ($ev->type) {
@ -179,7 +179,7 @@ while ($state != "") {
// we must ACK this message before dispatching a call.answered
$ev->Acknowledge();
// we already ACKed this message
$ev = "";
$ev = false;
$m = new Yate("call.answered");
$m->params["id"] = $ourcallid;
@ -207,7 +207,7 @@ while ($state != "") {
}
/* This is extremely important.
We MUST let messages return, handled or not */
if ($ev != "")
if ($ev)
$ev->Acknowledge();
break;
case "answer":

View File

@ -130,11 +130,11 @@ function gotDTMF($text)
while ($state != "") {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* Empty events are normal in non-blocking operation.
This is an opportunity to do idle tasks and check timers */
if ($ev == "")
if ($ev === true)
continue;
/* If we reached here we should have a valid object */
switch ($ev->type) {
@ -147,7 +147,7 @@ while ($state != "") {
// we must ACK this message before dispatching a call.answered
$ev->Acknowledge();
// we already ACKed this message
$ev = "";
$ev = false;
$m = new Yate("call.answered");
$m->params["id"] = $ourcallid;
@ -175,7 +175,7 @@ while ($state != "") {
}
/* This is extremely important.
We MUST let messages return, handled or not */
if ($ev != "")
if ($ev)
$ev->Acknowledge();
break;
case "answer":

View File

@ -20,11 +20,11 @@ Yate::Install("user.auth",10);
for (;;) {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* Empty events are normal in non-blocking operation.
This is an opportunity to do idle tasks and check timers */
if ($ev == "") {
if ($ev === true) {
// Yate::Output("PHP event: empty");
continue;
}

View File

@ -26,11 +26,11 @@ $m->Dispatch();
for (;;) {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* Empty events are normal in non-blocking operation.
This is an opportunity to do idle tasks and check timers */
if ($ev == "") {
if ($ev === true) {
// Yate::Output("PHP event: empty");
continue;
}

View File

@ -24,11 +24,11 @@ $m->Dispatch();
for (;;) {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* Empty events are normal in non-blocking operation.
This is an opportunity to do idle tasks and check timers */
if ($ev == "") {
if ($ev === true) {
Yate::Output("PHP event: empty");
continue;
}

View File

@ -324,10 +324,10 @@ function gotDTMF($text)
while ($state != "") {
$ev=Yate::GetEvent();
/* If Yate disconnected us then exit cleanly */
if ($ev == "EOF")
if ($ev === false)
break;
/* No need to handle empty events in this application */
if ($ev == "")
if ($ev === true)
continue;
/* If we reached here we should have a valid object */
switch ($ev->type) {
@ -341,7 +341,7 @@ while ($state != "") {
/* We must ACK this message before dispatching a call.answered */
$ev->Acknowledge();
/* Prevent a warning if trying to ACK this message again */
$ev = "";
$ev = false;
/* Signal we are answering the call */
$m = new Yate("call.answered");
@ -374,7 +374,7 @@ while ($state != "") {
}
/* This is extremely important.
We MUST let messages return, handled or not */
if ($ev != "")
if ($ev)
$ev->Acknowledge();
break;
case "answer":

View File

@ -3149,12 +3149,25 @@ public:
*/
virtual bool terminate() = 0;
/**
* Check if the last error code indicates a retryable condition
* @return True if error was temporary and operation should be retried
*/
virtual bool canRetry() const;
/**
* Check if this stream is valid
* @return True if the stream is valid, false if it's invalid or closed
*/
virtual bool valid() const = 0;
/**
* Set the blocking or non-blocking operation mode of the stream
* @param block True if I/O operations should block, false for non-blocking
* @return True if operation was successfull, false if an error occured
*/
virtual bool setBlocking(bool block = true);
/**
* Write data to a connected stream
* @param buffer Buffer for data transfer
@ -3287,6 +3300,12 @@ public:
inline HANDLE handle() const
{ return m_handle; }
/**
* Check if the last error code indicates a retryable condition
* @return True if error was temporary and operation should be retried
*/
virtual bool canRetry() const;
/**
* Check if this file is valid
* @return True if the file is valid, false if it's invalid or closed
@ -3299,6 +3318,13 @@ public:
*/
static HANDLE invalidHandle();
/**
* Set the blocking or non-blocking operation mode of the file
* @param block True if I/O operations should block, false for non-blocking
* @return True if operation was successfull, false if an error occured
*/
virtual bool setBlocking(bool block = true);
/**
* Write data to an open file
* @param buffer Buffer for data transfer
@ -3419,7 +3445,7 @@ public:
* Check if the last error code indicates a retryable condition
* @return True if error was temporary and operation should be retried
*/
bool canRetry() const;
virtual bool canRetry() const;
/**
* Check if this socket is valid
@ -3469,7 +3495,7 @@ public:
* @param block True if I/O operations should block, false for non-blocking
* @return True if operation was successfull, false if an error occured
*/
bool setBlocking(bool block = true);
virtual bool setBlocking(bool block = true);
/**
* Associates the socket with a local address