yate/modules/extmodule.cpp

2161 lines
55 KiB
C++

/**
* extmodule.cpp
* This file is part of the YATE Project http://YATE.null.ro
*
* External module handler
*
* Yet Another Telephony Engine - a fully featured software PBX and IVR
* Copyright (C) 2004-2017 Null Team
* Portions copyright (C) 2005 Maciek Kaminski
*
* This software is distributed under multiple licenses;
* see the COPYING file in the main directory for licensing
* information for this specific distribution.
*
* This use of this software may be subject to additional restrictions.
* See the LEGAL file in the main directory for details.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#include <yatephone.h>
#ifdef _WINDOWS
#include <process.h>
#else
#include <yatepaths.h>
#include <sys/stat.h>
#include <sys/wait.h>
#endif
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
using namespace TelEngine;
namespace { // anonymous
// Minimum length of the incoming line buffer
#define MIN_INCOMING_LINE 2048
// Default length of the incoming line buffer
#define DEF_INCOMING_LINE 8192
// Maximum length of the incoming line buffer
#define MAX_INCOMING_LINE 65536
// Default maximum messages queued in a receiver
#define DEF_MAXQUEUE 1000
// Maximum maximum messages queued in a receiver
#define MAX_MAXQUEUE 10000
// Default message timeout in milliseconds
#define MSG_TIMEOUT 10000
// Safety wait time after we flushed watchers, relays or messages (in ms)
#define WAIT_FLUSH 5
static Configuration s_cfg;
static ObjList s_chans;
static ObjList s_modules;
static Mutex s_mutex(true,"ExtModule");
static Mutex s_uses(false,"ExtModUse");
static int s_waitFlush = WAIT_FLUSH;
static int s_timeout = MSG_TIMEOUT;
static int s_maxQueue = DEF_MAXQUEUE;
static bool s_settime = false;
static bool s_timebomb = false;
static bool s_pluginSafe = true;
static const char* s_trackName = 0;
static const char* s_cmds[] = {
"info",
"start",
"stop",
"restart",
"execute",
0
};
static const char s_helpExternalCmd[] = "external [info] [stop scriptname] [[start|restart] scriptname [parameter]] [execute progname [parameter]]";
static const char s_helpExternalInfo[] = "List, (re)start and stop scripts or execute an external program";
class ExtModReceiver;
class ExtModChan;
class ExtModSource : public ThreadedSource
{
public:
ExtModSource(Stream* str, ExtModChan* chan);
~ExtModSource();
virtual void run();
private:
Stream* m_str;
unsigned m_brate;
unsigned m_total;
ExtModChan* m_chan;
};
class ExtModConsumer : public DataConsumer
{
public:
ExtModConsumer(Stream* str);
~ExtModConsumer();
virtual unsigned long Consume(const DataBlock& data, unsigned long timestamp, unsigned long flags);
private:
Stream* m_str;
unsigned m_total;
};
class ExtModChan : public CallEndpoint
{
public:
enum {
NoChannel,
DataNone,
DataRead,
DataWrite,
DataBoth
};
ExtModChan(ExtModReceiver* recv);
static ExtModChan* build(const char* file, const char* args, int type);
~ExtModChan();
virtual void disconnected(bool final, const char* reason);
inline ExtModReceiver* receiver() const
{ return m_recv; }
inline void setRecv(ExtModReceiver* recv)
{ m_recv = recv; }
inline void setRunning(bool running)
{ m_running = running; }
inline bool running() const
{ return m_running; }
inline void setDisconn(bool disconn)
{ m_disconn = disconn; }
inline bool disconn() const
{ return m_disconn; }
inline void setId(const String& id)
{ CallEndpoint::setId(id); }
inline const Message* waitMsg() const
{ return m_waitRet; }
inline void waitMsg(const Message* msg)
{ m_waitRet = msg; }
inline bool waiting() const
{ return m_waiting; }
inline void waiting(bool wait)
{ m_waiting = wait; }
private:
ExtModChan(const char* file, const char* args, int type);
ExtModReceiver *m_recv;
const Message* m_waitRet;
int m_type;
bool m_running;
bool m_disconn;
bool m_waiting;
};
// Great idea - thanks, Maciek!
class ExtMessage : public Message
{
YCLASS(ExtMessage,Message)
public:
inline ExtMessage()
: Message(""),
m_receiver(0), m_accepted(false)
{ }
virtual ~ExtMessage();
void startup(ExtModReceiver* recv);
virtual void dispatched(bool accepted);
inline bool belongsTo(ExtModReceiver* recv) const
{ return m_receiver == recv; }
inline int decode(const char* str)
{ return Message::decode(str,m_id); }
inline const String& id() const
{ return m_id; }
private:
ExtModReceiver* m_receiver;
String m_id;
bool m_accepted;
};
class MsgHolder : public GenObject, public Semaphore
{
public:
MsgHolder(Message &msg);
Message &m_msg;
bool m_ret;
String m_id;
bool decode(const char *s);
inline const Message* msg() const
{ return &m_msg; }
};
// Yet Another of Maciek's ideas
class MsgWatcher : public MessagePostHook
{
public:
inline MsgWatcher(ExtModReceiver* receiver)
: m_receiver(receiver)
{ }
virtual void dispatched(const Message& msg, bool handled);
bool addWatched(const String& name);
bool delWatched(const String& name);
void clear();
protected:
virtual void destroyed();
private:
ExtModReceiver* m_receiver;
ObjList m_watched;
};
class ExtModReceiver : public MessageReceiver, public Mutex, public DebugEnabler
{
friend class MsgWatcher;
public:
enum {
RoleUnknown,
RoleGlobal,
RoleChannel
};
static ExtModReceiver* build(const char *script, const char *args, bool ref = false,
File* ain = 0, File* aout = 0, ExtModChan *chan = 0);
static ExtModReceiver* build(const char* name, Stream* io, ExtModChan* chan = 0,
int role = RoleUnknown, const char* conn = 0);
static ExtModReceiver* find(const String& script, const String& arg);
virtual void destruct();
virtual bool received(Message& msg, int id);
bool processLine(const char* line);
bool outputLine(const char* line);
void reportError(const char* line);
void returnMsg(const Message* msg, const char* id, bool accepted);
bool addWatched(const String& name);
bool delWatched(const String& name);
bool start();
void run();
void cleanup();
bool flush();
void die(bool clearChan = true);
bool useUnlocked();
bool use();
bool unuse();
inline const String& scriptFile() const
{ return m_script; }
inline const String& commandArg() const
{ return m_args; }
inline bool selfWatch() const
{ return m_selfWatch; }
inline void setRestart(bool restart)
{ m_restart = restart; }
inline bool dead() const
{ return m_dead || m_quit || (m_use <= 0); }
void describe(String& rval) const;
private:
ExtModReceiver(const char* script, const char* args,
File* ain, File* aout, ExtModChan* chan);
ExtModReceiver(const char* name, Stream* io, ExtModChan* chan,
int role, const char* conn);
bool create(const char* script, const char* args);
void closeIn();
void closeOut();
void closeAudio();
bool outputLineInternal(const char* line, int len);
int m_role;
bool m_dead;
bool m_quit;
int m_use;
int m_qLength;
pid_t m_pid;
Stream* m_in;
Stream* m_out;
File* m_ain;
File* m_aout;
ExtModChan* m_chan;
MsgWatcher* m_watcher;
bool m_selfWatch;
bool m_reenter;
bool m_setdata;
bool m_settime;
bool m_writing;
int m_maxQueue;
int m_timeout;
bool m_timebomb;
bool m_restart;
bool m_scripted;
DataBlock m_buffer;
String m_script, m_args;
ObjList m_waiting;
ObjList m_relays;
String m_trackName;
String m_reason;
String m_debugName;
};
class ExtThread : public Thread
{
public:
ExtThread(ExtModReceiver* receiver)
: Thread("ExtMod Receiver"), m_receiver(receiver)
{ }
virtual void run()
{ m_receiver->run(); }
virtual void cleanup()
{ m_receiver->cleanup(); }
private:
ExtModReceiver* m_receiver;
};
class ExtModHandler;
class ExtModulePlugin : public Plugin
{
public:
ExtModulePlugin();
~ExtModulePlugin();
virtual void initialize();
virtual bool isBusy() const;
private:
ExtModHandler *m_handler;
};
INIT_PLUGIN(ExtModulePlugin);
class ExtModHandler : public MessageHandler
{
public:
ExtModHandler(const char *name, unsigned prio)
: MessageHandler(name,prio,__plugin.name())
{ }
virtual bool received(Message &msg);
};
class ExtModCommand : public MessageHandler
{
public:
ExtModCommand()
: MessageHandler("engine.command",100,__plugin.name())
{ }
virtual bool received(Message &msg);
private:
bool complete(const String& partLine, const String& partWord, String& rval) const;
};
class ExtModStatus : public MessageHandler
{
public:
ExtModStatus()
: MessageHandler("engine.status",110,__plugin.name())
{ }
virtual bool received(Message &msg);
};
class ExtModHelp : public MessageHandler
{
public:
ExtModHelp()
: MessageHandler("engine.help",100,__plugin.name())
{ }
virtual bool received(Message& msg);
};
class ExtListener : public Thread
{
public:
ExtListener(const char* name);
virtual bool init(const NamedList& sect);
virtual void run();
inline const String& name() const
{ return m_name; }
static ExtListener* build(const char* name, const NamedList& sect);
protected:
Socket m_socket;
String m_name;
int m_role;
};
static bool runProgram(const char *script, const char *args)
{
#ifdef _WINDOWS
int pid = ::_spawnl(_P_DETACH,script,args,NULL);
if (pid < 0) {
Debug(DebugWarn, "Failed to _spawnl(): %d: %s", errno, strerror(errno));
return false;
}
#else
int pid = ::fork();
if (pid < 0) {
Debug(DebugWarn, "Failed to fork(): %d: %s", errno, strerror(errno));
return false;
}
if (!pid) {
// In child - terminate all other threads if needed
Thread::preExec();
// Try to immunize child from ^C and ^\ the console may receive
::signal(SIGINT,SIG_IGN);
::signal(SIGQUIT,SIG_IGN);
// And restore default handlers for other signals
::signal(SIGTERM,SIG_DFL);
::signal(SIGHUP,SIG_DFL);
// Blindly close everything but stdin/out/err
for (int f=STDERR_FILENO+1;f<1024;f++)
::close(f);
// Execute script
if (debugAt(DebugInfo))
::fprintf(stderr, "Execing program '%s' '%s'\n", script, args);
::execl(script, script, args, (char *)NULL);
::fprintf(stderr, "Failed to execute '%s': %d: %s\n", script, errno, strerror(errno));
// Shit happened. Die as quick and brutal as possible
::_exit(1);
}
#endif
Debug(DebugInfo,"Launched external program %s", script);
return true;
}
static void adjustPath(String& script)
{
if (script.null() || script.startsWith(Engine::pathSeparator()))
return;
String tmp = Engine::sharedPath();
tmp << Engine::pathSeparator() << "scripts";
tmp = s_cfg.getValue("general","scripts_dir",tmp);
Engine::runParams().replaceParams(tmp);
if (!tmp.endsWith(Engine::pathSeparator()))
tmp += Engine::pathSeparator();
script = tmp + script;
}
ExtModSource::ExtModSource(Stream* str, ExtModChan* chan)
: m_str(str), m_brate(16000), m_total(0), m_chan(chan)
{
Debug(DebugAll,"ExtModSource::ExtModSource(%p) [%p]",str,this);
if (m_str) {
chan->setRunning(true);
start("ExtMod Source");
}
}
ExtModSource::~ExtModSource()
{
Debug(DebugAll,"ExtModSource::~ExtModSource() [%p] total=%u",this,m_total);
m_chan->setRunning(false);
if (m_str) {
Stream* tmp = m_str;
m_str = 0;
delete tmp;
}
}
void ExtModSource::run()
{
char data[320];
int r = 1;
u_int64_t tpos = Time::now();
while ((r > 0) && looping()) {
if (!m_str) {
Thread::yield();
continue;
}
r = m_str->readData(data,sizeof(data));
if (r < 0) {
if (errno == EINTR) {
r = 1;
continue;
}
break;
}
// TODO: allow data to provide its own rate
int64_t dly = tpos - Time::now();
if (dly > 0) {
XDebug("ExtModSource",DebugAll,"Sleeping for " FMT64 " usec",dly);
Thread::usleep((unsigned long)dly);
}
if (r <= 0)
continue;
DataBlock buf(data,r,false);
Forward(buf,m_total/2);
buf.clear(false);
m_total += r;
tpos += (r*(u_int64_t)1000000/m_brate);
}
Debug(DebugAll,"ExtModSource [%p] end of data total=%u",this,m_total);
m_chan->setRunning(false);
}
ExtModConsumer::ExtModConsumer(Stream* str)
: m_str(str), m_total(0)
{
Debug(DebugAll,"ExtModConsumer::ExtModConsumer(%p) [%p]",str,this);
}
ExtModConsumer::~ExtModConsumer()
{
Debug(DebugAll,"ExtModConsumer::~ExtModConsumer() [%p] total=%u",this,m_total);
if (m_str) {
Stream* tmp = m_str;
m_str = 0;
delete tmp;
}
}
unsigned long ExtModConsumer::Consume(const DataBlock& data, unsigned long timestamp, unsigned long flags)
{
if ((m_str) && !data.null()) {
m_str->writeData(data);
m_total += data.length();
return invalidStamp();
}
return 0;
}
ExtModChan* ExtModChan::build(const char* file, const char* args, int type)
{
ExtModChan* chan = new ExtModChan(file,args,type);
if (!chan->m_recv) {
chan->destruct();
return 0;
}
return chan;
}
ExtModChan::ExtModChan(const char* file, const char* args, int type)
: CallEndpoint("ExtModule"),
m_recv(0), m_waitRet(0), m_type(type),
m_running(false), m_disconn(false), m_waiting(false)
{
Debug(DebugAll,"ExtModChan::ExtModChan(%d) [%p]",type,this);
File* reader = 0;
File* writer = 0;
switch (m_type) {
case DataWrite:
case DataBoth:
{
reader = new File;
File* tmp = new File;
if (File::createPipe(*reader,*tmp)) {
setConsumer(new ExtModConsumer(tmp));
getConsumer()->deref();
}
else
delete tmp;
}
}
switch (m_type) {
case DataRead:
case DataBoth:
{
writer = new File;
File* tmp = new File;
if (File::createPipe(*tmp,*writer)) {
setSource(new ExtModSource(tmp,this));
getSource()->deref();
}
else
delete tmp;
}
}
s_mutex.lock();
s_chans.append(this);
s_mutex.unlock();
m_recv = ExtModReceiver::build(file,args,true,reader,writer,this);
}
ExtModChan::ExtModChan(ExtModReceiver* recv)
: CallEndpoint("ExtModule"),
m_recv(recv), m_waitRet(0), m_type(DataNone),
m_running(false), m_disconn(false), m_waiting(false)
{
Debug(DebugAll,"ExtModChan::ExtModChan(%p) [%p]",recv,this);
s_mutex.lock();
s_chans.append(this);
s_mutex.unlock();
}
ExtModChan::~ExtModChan()
{
Debugger debug(DebugAll,"ExtModChan::~ExtModChan()"," [%p]",this);
s_mutex.lock();
s_chans.remove(this,false);
ExtModReceiver* recv = m_recv;
m_recv = 0;
s_mutex.unlock();
setSource();
setConsumer();
if (recv)
recv->die(false);
}
void ExtModChan::disconnected(bool final, const char *reason)
{
Debug(DebugAll,"ExtModChan::disconnected() '%s' [%p]",reason,this);
if (final || Engine::exiting())
return;
if (m_disconn) {
Message* m = new Message("chan.disconnected");
m->userData(this);
m->addParam("id",id());
m->addParam("module","external");
if (m_recv)
m->addParam("address",m_recv->scriptFile());
if (reason)
m->addParam("reason",reason);
String peerId;
if (getPeerId(peerId) && peerId)
m->addParam("peerid",peerId);
Engine::enqueue(m);
}
}
MsgHolder::MsgHolder(Message &msg)
: m_msg(msg), m_ret(false)
{
// the address of this object should be unique
char buf[64];
::sprintf(buf,"%p.%ld",this,Random::random());
m_id = buf;
}
bool MsgHolder::decode(const char *s)
{
return (m_msg.decode(s,m_ret,m_id) == -2);
}
ExtMessage::~ExtMessage()
{
if (m_receiver) {
m_receiver->returnMsg(this,m_id,m_accepted);
m_receiver->unuse();
}
}
void ExtMessage::startup(ExtModReceiver* recv)
{
if (recv && m_id && recv->use())
m_receiver = recv;
Engine::enqueue(this);
}
void ExtMessage::dispatched(bool accepted)
{
m_accepted = accepted;
Message::dispatched(accepted);
}
void MsgWatcher::destroyed()
{
clear();
MessagePostHook::destroyed();
}
void MsgWatcher::dispatched(const Message& msg, bool handled)
{
Lock lock(s_uses);
ExtModReceiver* recv = m_receiver;
if (!recv || recv->dead() || (recv->m_watcher != this) || !recv->useUnlocked())
return;
if (!lock.acquire(recv) || !m_receiver || recv->dead()) {
lock.drop();
recv->unuse();
return;
}
if (!recv->selfWatch()) {
// check if the message was generated by ourselves - avoid reentrance
ExtMessage* m = YOBJECT(ExtMessage,&msg);
if (m && m->belongsTo(recv)) {
recv->unuse();
return;
}
}
ObjList* l = m_watched.skipNull();
for (; l; l = l->skipNext()) {
const String* s = static_cast<const String*>(l->get());
if (s->null() || (*s == msg))
break;
}
if (l && m_receiver) {
lock.drop();
recv->returnMsg(&msg,"",handled);
}
recv->unuse();
}
bool MsgWatcher::addWatched(const String& name)
{
if (m_watched.find(name))
return false;
// wildcard watches will be inserted first for speed reason
if (name.null())
m_watched.insert(new String);
else
m_watched.append(new String(name));
return true;
}
bool MsgWatcher::delWatched(const String& name)
{
GenObject* obj = m_watched[name];
if (obj) {
m_watched.remove(obj);
return true;
}
return false;
}
void MsgWatcher::clear()
{
Engine::self()->setHook(this,true);
if (!m_receiver)
return;
s_uses.lock();
ExtModReceiver* recv = m_receiver;
m_receiver = 0;
if (recv && (recv->m_watcher == this))
recv->m_watcher = 0;
s_uses.unlock();
}
ExtModReceiver* ExtModReceiver::build(const char* script, const char* args, bool ref,
File* ain, File* aout, ExtModChan* chan)
{
ExtModReceiver* recv = new ExtModReceiver(script,args,ain,aout,chan);
if (ref) {
if (!recv->use())
return 0;
if (recv->start())
return recv;
recv->unuse();
return 0;
}
return recv->start() ? recv : 0;
}
ExtModReceiver* ExtModReceiver::build(const char* name, Stream* io, ExtModChan* chan,
int role, const char* conn)
{
ExtModReceiver* recv = new ExtModReceiver(name,io,chan,role,conn);
return recv->start() ? recv : 0;
}
ExtModReceiver* ExtModReceiver::find(const String& script, const String& arg)
{
Lock lock(s_mutex);
ObjList *l = &s_modules;
for (; l; l=l->next()) {
ExtModReceiver *r = static_cast<ExtModReceiver *>(l->get());
if (r && (r->scriptFile() == script)) {
if (arg.null() || (r->commandArg() == arg))
return r;
}
}
return 0;
}
bool ExtModReceiver::useUnlocked()
{
if (m_use <= 0)
return false;
++m_use;
return true;
}
bool ExtModReceiver::use()
{
s_uses.lock();
bool ok = (m_use > 0);
if (ok)
++m_use;
s_uses.unlock();
return ok;
}
bool ExtModReceiver::unuse()
{
s_uses.lock();
int u = m_use - 1;
if (u >= 0)
m_use = u;
s_uses.unlock();
if (!u)
destruct();
return (u <= 0);
}
ExtModReceiver::ExtModReceiver(const char* script, const char* args, File* ain, File* aout, ExtModChan* chan)
: Mutex(true,"ExtModReceiver"),
m_role(RoleUnknown), m_dead(false), m_quit(false), m_use(1), m_qLength(0), m_pid(-1),
m_in(0), m_out(0), m_ain(ain), m_aout(aout),
m_chan(chan), m_watcher(0),
m_selfWatch(false), m_reenter(false), m_setdata(true), m_settime(s_settime), m_writing(false),
m_maxQueue(s_maxQueue), m_timeout(s_timeout), m_timebomb(s_timebomb), m_restart(false), m_scripted(false),
m_buffer(0,DEF_INCOMING_LINE), m_script(script), m_args(args), m_trackName(s_trackName)
{
debugChain(&__plugin);
debugName(m_script);
Debug(DebugAll,"ExtModReceiver::ExtModReceiver(\"%s\",\"%s\") [%p]",script,args,this);
m_script.trimBlanks();
m_args.trimBlanks();
m_role = chan ? RoleChannel : RoleGlobal;
s_mutex.lock();
s_modules.append(this);
s_mutex.unlock();
}
ExtModReceiver::ExtModReceiver(const char* name, Stream* io, ExtModChan* chan, int role, const char* conn)
: Mutex(true,"ExtModReceiver"),
m_role(role), m_dead(false), m_quit(false), m_use(1), m_qLength(0), m_pid(-1),
m_in(io), m_out(io), m_ain(0), m_aout(0),
m_chan(chan), m_watcher(0),
m_selfWatch(false), m_reenter(false), m_setdata(true), m_settime(s_settime), m_writing(false),
m_maxQueue(s_maxQueue), m_timeout(s_timeout), m_timebomb(s_timebomb), m_restart(false), m_scripted(false),
m_buffer(0,DEF_INCOMING_LINE), m_script(name), m_args(conn), m_trackName(s_trackName)
{
debugChain(&__plugin);
debugName(m_script);
Debug(DebugAll,"ExtModReceiver::ExtModReceiver(\"%s\",%p,%p) [%p]",name,io,chan,this);
m_script.trimBlanks();
m_args.trimBlanks();
if (chan)
m_role = RoleChannel;
s_mutex.lock();
s_modules.append(this);
s_mutex.unlock();
}
void ExtModReceiver::destruct()
{
Debug(DebugAll,"ExtModReceiver::destruct() pid=%d [%p]",m_pid,this);
lock();
// One destruction is plenty enough
m_use = -1;
s_mutex.lock();
s_modules.remove(this,false);
s_mutex.unlock();
die();
if (m_pid > 1)
Debug(DebugWarn,"ExtModReceiver::destruct() pid=%d [%p]",m_pid,this);
closeAudio();
Stream* tmp = m_in;
m_in = 0;
if (tmp == m_out)
m_out = 0;
delete tmp;
tmp = m_out;
m_out = 0;
delete tmp;
unlock();
Thread::yield();
GenObject::destruct();
}
void ExtModReceiver::closeIn()
{
Stream* tmp = m_in;
if (tmp)
tmp->terminate();
}
void ExtModReceiver::closeOut()
{
Stream* tmp = m_out;
if (tmp)
tmp->terminate();
}
void ExtModReceiver::closeAudio()
{
if (m_ain) {
delete m_ain;
m_ain = 0;
}
if (m_aout) {
delete m_aout;
m_aout = 0;
}
}
bool ExtModReceiver::start()
{
if (m_pid < 0) {
ExtThread *ext = new ExtThread(this);
if (!ext->startup()) {
// self destruct here since there is no thread to do it later
unuse();
return false;
}
while (m_pid < 0)
Thread::yield();
}
return (m_pid >= 0);
}
bool ExtModReceiver::flush()
{
lock();
MsgWatcher* w = m_watcher;
m_watcher = 0;
bool needWait = !!w;
if (w) {
w->clear();
Thread::yield();
TelEngine::destruct(w);
}
// Make sure we release all pending messages and not accept new ones
needWait = needWait || (m_relays.count() != 0);
if (s_pluginSafe)
m_relays.clear();
else {
ObjList *p = &m_relays;
for (; p; p=p->next())
p->setDelete(false);
}
bool flushed = false;
if (m_waiting.get()) {
Debug(DebugInfo,"ExtModReceiver releasing %u pending messages [%p]",m_qLength,this);
m_waiting.clear();
m_qLength = 0;
needWait = flushed = true;
}
unlock();
if (needWait && s_pluginSafe) {
int ms = s_waitFlush;
// During shutdown longer delays are not acceptable
if ((ms > WAIT_FLUSH) && Engine::exiting())
ms = WAIT_FLUSH;
DDebug(DebugAll,"ExtModReceiver sleeping %d ms [%p]",ms,this);
Thread::msleep(ms);
}
return flushed;
}
void ExtModReceiver::die(bool clearChan)
{
#ifdef DEBUG
Debugger debug(DebugAll,"ExtModReceiver::die()"," pid=%d dead=%s [%p]",
m_pid,m_dead ? "yes" : "no",this);
#else
Debug(DebugAll,"ExtModReceiver::die() pid=%d dead=%s [%p]",
m_pid,m_dead ? "yes" : "no",this);
#endif
if (m_dead)
return;
Lock mylock(this);
if (m_dead) {
DDebug(DebugInfo,"ExtModReceiver::die() pid=%d is already dead [%p]",m_pid,this);
return;
}
m_dead = true;
m_quit = true;
use();
RefPointer<ExtModChan> chan = m_chan;
m_chan = 0;
if (chan)
chan->setRecv(0);
mylock.drop();
if (m_scripted && (m_role == RoleGlobal))
Output("Unloading external module '%s' '%s'",m_script.c_str(),m_args.safe());
// Give the external script a chance to die gracefully
closeOut();
if (m_pid > 1) {
Debug(DebugAll,"ExtModReceiver::die() waiting for pid=%d to die [%p]",m_pid,this);
for (int i=0; i<100; i++) {
Thread::yield();
if (m_pid <= 0)
break;
}
}
if (m_pid > 1)
Debug(DebugInfo,"ExtModReceiver::die() pid=%d did not exit? [%p]",m_pid,this);
// Close the stdout pipe before terminating the process
closeIn();
// Release relays and messages since no confirmation can be received anymore
flush();
#ifndef _WINDOWS
if (m_pid > 1)
::kill(m_pid,SIGTERM);
#endif
if (chan && clearChan)
chan->disconnect(m_reason);
if (m_restart && !Engine::exiting()) {
Debug(DebugMild,"Restarting external '%s' '%s'",m_script.safe(),m_args.safe());
ExtModReceiver::build(m_script,m_args);
}
unuse();
}
bool ExtModReceiver::received(Message &msg, int id)
{
if (m_dead || m_quit)
return false;
if (!lock(m_timeout > 0 ? ((long)m_timeout * 1000) : -1)) {
Alarm("extmodule","performance",DebugWarn,
"Failed to lock to queue message (%p) '%s' for %d msec [%p]",
&msg,msg.c_str(),m_timeout,this);
return false;
}
// check if we are no longer running
bool ok = (m_pid > 0) && !m_dead && m_in && m_in->valid() && m_out && m_out->valid();
if (ok && !m_reenter) {
// check if the message was generated by ourselves - avoid reentrance
ExtMessage* m = YOBJECT(ExtMessage,&msg);
if (m && m->belongsTo(this))
ok = false;
}
if (ok && m_maxQueue && (m_qLength >= m_maxQueue)) {
Debug(DebugWarn,"ExtMod already having %u queued messages [%p]",m_qLength,this);
ok = false;
}
if (!ok) {
unlock();
return false;
}
use();
bool fail = false;
u_int64_t tout = (m_timeout > 0) ? Time::now() + 1000 * m_timeout : 0;
MsgHolder h(msg);
if (outputLine(msg.encode(h.m_id))) {
m_qLength++;
m_waiting.append(&h)->setDelete(false);
DDebug(DebugAll,"ExtMod queued message #%u %p '%s' [%p]",m_qLength,&msg,msg.c_str(),this);
}
else {
Debug(DebugWarn,"ExtMod could not queue message %p '%s' [%p]",&msg,msg.c_str(),this);
ok = false;
fail = true;
}
unlock();
// would be nice to lock the MsgHolder and wait for it to unlock from some
// other thread - unfortunately this does not work with all mutexes
// sorry, Maciek - have to do it work in Windows too :-(
while (ok) {
h.lock(Thread::idleUsec());
lock();
ok = (m_waiting.find(&h) != 0);
if (ok && tout && (Time::now() > tout)) {
Alarm("extmodule","performance",DebugWarn,"Message %p '%s' did not return in %d msec [%p]",
&msg,msg.c_str(),m_timeout,this);
if (m_waiting.remove(&h,false) && (m_qLength > 0))
m_qLength--;
ok = false;
fail = true;
}
unlock();
}
DDebug(DebugAll,"ExtMod message %p '%s' returning %s [%p]",
&msg,msg.c_str(),String::boolText(h.m_ret),this);
if (fail && m_timebomb)
die();
unuse();
return h.m_ret;
}
bool ExtModReceiver::create(const char *script, const char *args)
{
#ifdef _WINDOWS
return false;
#else
String tmp(script);
int pid;
HANDLE ext2yate[2];
HANDLE yate2ext[2];
int x;
adjustPath(tmp);
script = tmp.c_str();
if (::pipe(ext2yate)) {
Debug(DebugWarn, "Unable to create ext->yate pipe: %s",strerror(errno));
return false;
}
if (pipe(yate2ext)) {
Debug(DebugWarn, "unable to create yate->ext pipe: %s", strerror(errno));
::close(ext2yate[0]);
::close(ext2yate[1]);
return false;
}
pid = ::fork();
if (pid < 0) {
Debug(DebugWarn, "Failed to fork(): %s", strerror(errno));
::close(yate2ext[0]);
::close(yate2ext[1]);
::close(ext2yate[0]);
::close(ext2yate[1]);
return false;
}
if (!pid) {
// In child - terminate all other threads if needed
Thread::preExec();
// Try to immunize child from ^C and ^\ the console may receive
::signal(SIGINT,SIG_IGN);
::signal(SIGQUIT,SIG_IGN);
// And restore default handlers for other signals
::signal(SIGTERM,SIG_DFL);
::signal(SIGHUP,SIG_DFL);
// Redirect stdin and out
::dup2(yate2ext[0], STDIN_FILENO);
::dup2(ext2yate[1], STDOUT_FILENO);
// Set audio in/out handlers
if (m_ain && m_ain->valid())
::dup2(m_ain->handle(), STDERR_FILENO+1);
else
::close(STDERR_FILENO+1);
if (m_aout && m_aout->valid())
::dup2(m_aout->handle(), STDERR_FILENO+2);
else
::close(STDERR_FILENO+2);
// Blindly close everything but stdin/out/err/audio
for (x=STDERR_FILENO+3;x<1024;x++)
::close(x);
// Execute script
if (debugAt(DebugInfo))
::fprintf(stderr, "Execing '%s' '%s'\n", script, args);
::execl(script, script, args, (char *)NULL);
::fprintf(stderr, "Failed to execute '%s': %s\n", script, strerror(errno));
// Shit happened. Die as quick and brutal as possible
::_exit(1);
}
if (m_role == RoleGlobal)
Output("Loading external module '%s' '%s'",m_script.c_str(),args);
else
Debug(DebugInfo,"Launched External Script '%s' '%s'",script,args);
m_in = new File(ext2yate[0]);
m_out = new File(yate2ext[1]);
// close what we're not using in the parent
close(ext2yate[1]);
close(yate2ext[0]);
closeAudio();
m_scripted = true;
m_pid = pid;
return true;
#endif
}
void ExtModReceiver::cleanup()
{
#ifdef DEBUG
Debugger debug(DebugAll,"ExtModReceiver::cleanup()"," [%p]",this);
#endif
#ifndef _WINDOWS
// We must call waitpid from here - same thread we started the child
if (m_pid > 1) {
// No thread switching if possible
closeOut();
Thread::yield();
int w = ::waitpid(m_pid, 0, WNOHANG);
if (w == 0) {
Debug(DebugWarn,"Process %d has not exited on closing stdin - we'll kill it",m_pid);
::kill(m_pid,SIGTERM);
Thread::yield();
w = ::waitpid(m_pid, 0, WNOHANG);
}
if (w == 0)
Debug(DebugWarn,"Process %d has still not exited yet?",m_pid);
else if ((w < 0) && (errno != ECHILD))
Debug(DebugMild,"Failed waitpid on %d: %s",m_pid,strerror(errno));
}
if (m_pid > 0)
m_pid = 0;
#endif
unuse();
}
void ExtModReceiver::run()
{
// the i/o streams may be already allocated
if (m_in && m_out)
m_pid = 1; // just an indicator, not really init ;-)
// we must do the forking from this thread so we can later wait() on it
else if (!create(m_script.safe(),m_args.safe())) {
m_pid = 0;
return;
}
if (m_in && !m_in->setBlocking(false))
Debug("ExtModule",DebugWarn,"Failed to set nonblocking mode, expect trouble [%p]",this);
int posinbuf = 0;
bool invalid = true;
DDebug(DebugAll,"ExtModReceiver::run() entering loop [%p]",this);
for (;;) {
use();
lock();
char* buffer = static_cast<char*>(m_buffer.data());
int bufspace = m_buffer.length() - posinbuf - 1;
int readsize = (m_in && bufspace) ? m_in->readData(buffer+posinbuf,bufspace) : 0;
unlock();
if (unuse() || m_dead)
return;
if (!bufspace) {
Debug("ExtModule",DebugWarn,"Overflow reading in buffer of length %u, closing [%p]",
m_buffer.length(),this);
return;
}
if (!readsize) {
if (m_in)
Debug("ExtModule",DebugInfo,"Read EOF on %p [%p]",m_in,this);
closeIn();
flush();
if (invalid)
Debug("ExtModule",DebugWarn,"Never got anything valid from terminated '%s' '%s'",
m_script.c_str(),m_args.safe());
if (m_chan && m_chan->running())
Thread::sleep(1);
break;
}
else if (readsize < 0) {
Lock mylock(this);
if (m_in && m_in->canRetry()) {
mylock.drop();
Thread::idle();
continue;
}
if (!m_quit)
Debug("ExtModule",DebugWarn,"Read error %d on %p [%p]",errno,m_in,this);
break;
}
XDebug(DebugAll,"ExtModReceiver::run() read %d [%p]",readsize,this);
int totalsize = readsize + posinbuf;
if (totalsize >= (int)m_buffer.length()) {
Debug("ExtModule",DebugWarn,"Overflow reading in buffer of length %u, closing [%p]",
m_buffer.length(),this);
return;
}
buffer[totalsize] = 0;
for (;;) {
char *eoline = ::strchr(buffer,'\n');
if (!eoline && ((int)::strlen(buffer) < totalsize))
eoline=buffer+::strlen(buffer);
if (!eoline)
break;
*eoline = 0;
if ((eoline > buffer) && (eoline[-1] == '\r'))
eoline[-1] = 0;
readsize = eoline-buffer+1;
if (buffer[0]) {
invalid = invalid && (buffer[0] != '%' || buffer[1] != '%');
use();
bool goOut = processLine(buffer);
if (unuse() || goOut)
return;
if (totalsize >= (int)m_buffer.length()) {
Debug("ExtModule",DebugWarn,"Lost data shrinking read buffer to %u, closing [%p]",
m_buffer.length(),this);
return;
}
}
totalsize -= readsize;
buffer = static_cast<char*>(m_buffer.data());
::memmove(buffer,buffer+readsize,totalsize+1);
}
posinbuf = totalsize;
}
}
bool ExtModReceiver::outputLine(const char* line)
{
if (TelEngine::null(line))
return true;
int len = ::strlen(line);
if (m_dead || !m_out || !m_out->valid() || !use())
return false;
uint64_t tout = (m_timeout > 0) ? (Time::now() + 1000 * (uint64_t)m_timeout) : 0;
for (;;) {
Lock mylock(this);
if (m_dead || !m_out || !m_out->valid()) {
unuse();
return false;
}
if (!m_writing) {
m_writing = true;
break;
}
if (tout && tout < Time::now()) {
if (!m_quit)
Alarm("extmodule","performance",DebugWarn,"Timeout %d msec for %d characters [%p]",
m_timeout,len,this);
unuse();
return false;
}
mylock.drop();
Thread::idle();
}
bool ok = outputLineInternal(line,len);
m_writing = false;
unuse();
return ok;
}
bool ExtModReceiver::outputLineInternal(const char* line, int len)
{
DDebug("ExtModReceiver",DebugAll,"outputLine len=%d '%s' [%p]",len,line,this);
// since m_out can be non-blocking (the socket) we have to loop
while (m_out && m_out->valid() && (len > 0) && !m_dead) {
int w = m_out->writeData(line,len);
if (w < 0) {
if (m_dead || !m_out || !m_out->canRetry())
return false;
}
else {
line += w;
len -= w;
}
if (len > 0)
Thread::idle();
}
char nl = '\n';
for (;;) {
if (m_dead || !m_out)
return false;
int w = m_out->writeData(&nl,1);
if ((w < 0) && m_out && m_out->canRetry())
w = 0;
if (w > 0)
return true;
if (w < 0)
return false;
Thread::idle();
}
}
void ExtModReceiver::reportError(const char* line)
{
Debug("ExtModReceiver",DebugWarn,"Error: '%s'", line);
outputLine("Error in: " + String(line));
}
void ExtModReceiver::returnMsg(const Message* msg, const char* id, bool accepted)
{
String ret(msg->encode(accepted,id));
if (!outputLine(ret) && m_timebomb)
die();
}
bool ExtModReceiver::addWatched(const String& name)
{
Lock mylock(this);
if (m_dead)
return false;
if (!m_watcher) {
m_watcher = new MsgWatcher(this);
Engine::self()->setHook(m_watcher);
}
return m_watcher->addWatched(name);
}
bool ExtModReceiver::delWatched(const String& name)
{
Lock mylock(this);
if (m_dead)
return false;
return m_watcher && m_watcher->delWatched(name);
}
bool ExtModReceiver::processLine(const char* line)
{
if (m_dead)
return false;
if (m_quit)
return true;
DDebug("ExtModReceiver",DebugAll,"processLine '%s'", line);
String id(line);
if (m_role == RoleUnknown) {
if (id.startSkip("%%>connect:",false)) {
int sep = id.find(':');
String role;
String chan;
String type;
if (sep >= 0) {
role = id.substr(0,sep);
id = id.substr(sep+1);
sep = id.find(':');
if (sep >= 0) {
chan = id.substr(0,sep);
type = id.substr(sep+1);
}
else
chan = id;
}
else
role = id;
DDebug("ExtModReceiver",DebugAll,"role '%s' chan '%s' type '%s'",
role.c_str(),chan.c_str(),type.c_str());
if (role == "global") {
m_role = RoleGlobal;
return false;
}
else if (role == "channel") {
m_role = RoleChannel;
return false;
}
Debug(DebugWarn,"Unknown role '%s' received [%p]",role.c_str(),this);
}
else
Debug(DebugWarn,"Expecting %%%%>connect, received '%s' [%p]",id.c_str(),this);
return true;
}
else if (id.startsWith("%%<message:")) {
Lock mylock(this);
ObjList *p = &m_waiting;
for (; p; p=p->next()) {
MsgHolder *msg = static_cast<MsgHolder *>(p->get());
if (msg && msg->decode(line)) {
DDebug("ExtModReceiver",DebugInfo,"Matched message %p [%p]",msg->msg(),this);
if (m_chan && (m_chan->waitMsg() == msg->msg())) {
DDebug("ExtModReceiver",DebugNote,"Entering wait mode on channel %p [%p]",m_chan,this);
m_chan->waitMsg(0);
m_chan->waiting(true);
}
msg->unlock();
if (p->remove(false) && (m_qLength > 0))
m_qLength--;
return false;
}
}
Debug("ExtModReceiver",(m_dead ? DebugInfo : DebugWarn),
"Unmatched%s message: %s [%p]",(m_dead ? " dead" : ""),line,this);
return false;
}
else if (id.startSkip("%%>install:",false)) {
int prio = 100;
id >> prio >> ":";
String fname;
String fvalue;
static const Regexp r("^\\([^:]*\\):\\([^:]*\\):\\?\\(.*\\)");
if (id.matches(r)) {
// a filter is specified
fname = String::msgUnescape(id.matchString(2));
fvalue = String::msgUnescape(id.matchString(3));
id = id.matchString(1);
}
// sanity checks
lock();
bool ok = id && !m_dead && !m_relays.find(id);
if (ok) {
MessageRelay *r = new MessageRelay(id,this,0,prio,m_trackName);
if (fname)
r->setFilter(fname,fvalue);
m_relays.append(r);
Engine::install(r);
}
unlock();
if (debugAt(DebugAll)) {
String tmp;
if (fname)
tmp << "filter: '" << fname << "'='" << fvalue << "' ";
tmp << (ok ? "ok" : "failed");
Debug("ExtModReceiver",DebugAll,"Install '%s', prio %d %s",
id.c_str(),prio,tmp.c_str());
}
String out("%%<install:");
out << prio << ":" << id << ":" << ok;
outputLine(out);
return false;
}
else if (id.startSkip("%%>uninstall:",false)) {
int prio = 0;
bool ok = false;
lock();
ObjList *p = &m_relays;
for (; p; p=p->next()) {
MessageRelay *r = static_cast<MessageRelay *>(p->get());
if (r && (*r == id)) {
prio = r->priority();
p->remove();
ok = true;
break;
}
}
unlock();
Debug("ExtModReceiver",DebugAll,"Uninstall '%s' %s", id.c_str(),ok ? "ok" : "failed");
String out("%%<uninstall:");
out << prio << ":" << id << ":" << ok;
outputLine(out);
return false;
}
else if (id.startSkip("%%>watch:",false)) {
bool ok = addWatched(id);
Debug("ExtModReceiver",DebugAll,"Watch '%s' %s", id.c_str(),ok ? "ok" : "failed");
String out("%%<watch:");
out << id << ":" << ok;
outputLine(out);
return false;
}
else if (id.startSkip("%%>unwatch:",false)) {
bool ok = delWatched(id);
Debug("ExtModReceiver",DebugAll,"Unwatch '%s' %s", id.c_str(),ok ? "ok" : "failed");
String out("%%<unwatch:");
out << id << ":" << ok;
outputLine(out);
return false;
}
else if (id.startSkip("%%>output:",false)) {
id.trimBlanks();
Output("%s",id.safe());
return false;
}
else if (id.startSkip("%%>debug:",false)) {
int pos = id.find(':');
if (pos > 0) {
int level = id.substr(0,pos).toInteger(DebugAll,0,DebugTest,DebugAll);
Debug(this,level,"%s",String::msgUnescape(id.substr(pos + 1)).safe());
return false;
}
}
else if (id.startSkip("%%>setlocal:",false)) {
int col = id.find(':');
if (col > 0) {
String val(id.substr(col+1));
val.trimBlanks();
id = id.substr(0,col);
bool ok = false;
Lock mylock(this);
if (m_dead)
return false;
if (m_chan && (id == "id")) {
if (val.null())
val = m_chan->id();
else
m_chan->setId(val);
ok = true;
}
else if (m_chan && (id == "disconnected")) {
m_chan->setDisconn(val.toBoolean(m_chan->disconn()));
val = m_chan->disconn();
ok = true;
}
else if (id == "trackparam") {
if (val.null())
val = m_trackName;
else
m_trackName = val;
ok = true;
}
else if (id == "reason") {
m_reason = val;
ok = true;
}
else if (id == "timeout") {
m_timeout = val.toInteger(m_timeout);
val = m_timeout;
ok = true;
}
else if (id == "timebomb") {
m_timebomb = val.toBoolean(m_timebomb);
val = m_timebomb;
ok = true;
}
else if (id == "maxqueue") {
m_maxQueue = val.toInteger(m_maxQueue,0,0,MAX_MAXQUEUE);
val = m_maxQueue;
ok = true;
}
else if (id == "bufsize") {
unsigned int len = val.toInteger(m_buffer.length(),0,
MIN_INCOMING_LINE,MAX_INCOMING_LINE);
if (len > m_buffer.length())
m_buffer.append(DataBlock(0,len - m_buffer.length()));
else if (len < m_buffer.length())
m_buffer.assign(m_buffer.data(),len);
val = m_buffer.length();
ok = true;
}
else if (id == "restart") {
m_restart = m_scripted && (RoleGlobal == m_role) && val.toBoolean(m_restart);
val = m_restart;
ok = true;
}
else if (id == "reenter") {
m_reenter = val.toBoolean(m_reenter);
val = m_reenter;
ok = true;
}
else if (id == "setdata") {
m_setdata = val.toBoolean(m_setdata);
val = m_setdata;
ok = true;
}
else if (id == "settime") {
m_settime = val.toBoolean(m_settime);
val = m_setdata;
ok = true;
}
else if (id == "selfwatch") {
m_selfWatch = val.toBoolean(m_selfWatch);
val = m_selfWatch;
ok = true;
}
else if (id.startsWith("engine.")) {
// keep the index in substr in sync with length of "engine."
const NamedString* param = Engine::runParams().getParam(id.substr(7));
ok = val.null() && param;
val = param;
}
else if (id.startsWith("config.")) {
ok = val.null();
// keep the index in substr in sync with length of "config."
val = id.substr(7);
int sep = val.find('.');
if (sep > 0) {
const NamedString* key = Engine::config().getKey(val.substr(0,sep).trimBlanks(),
val.substr(sep+1).trimBlanks());
if (key)
val = *key;
else {
val.clear();
ok = false;
}
}
else {
ok = (Engine::config().getSection(val) != 0);
val.clear();
}
}
else if (id.startsWith("loaded.")) {
ok = val.null();
// keep the index in substr in sync with length of "loaded."
val = Engine::self()->pluginLoaded(id.substr(7));
}
else if (id == "runid") {
ok = val.null();
val = Engine::runId();
}
else if (id == YSTRING("debuglevel")) {
ok = true;
if (val)
debugLevel(val.toInteger(DebugAll,0,DebugTest,DebugAll));
val = debugLevel();
}
else if (id == YSTRING("debugname")) {
ok = true;
if (val && !m_debugName) {
m_debugName = val;
debugName(m_debugName);
}
else
val = debugName();
}
DDebug("ExtModReceiver",DebugAll,"Set '%s'='%s' %s",
id.c_str(),val.c_str(),ok ? "ok" : "failed");
String out("%%<setlocal:");
out << id << ":" << val << ":" << ok;
outputLine(out);
return false;
}
}
else if (id == "%%>quit") {
m_quit = true;
outputLine("%%<quit");
return true;
}
else {
ExtMessage* m = new ExtMessage;
if (m->decode(line) == -2) {
DDebug("ExtModReceiver",DebugAll,"Created message %p '%s' [%p]",m,m->c_str(),this);
lock();
bool note = true;
while (!m_dead && m_chan && m_chan->waiting()) {
if (note) {
note = false;
Debug("ExtModReceiver",DebugNote,"Waiting before enqueueing new message %p '%s' [%p]",
m,m->c_str(),this);
}
unlock();
Thread::yield();
if (m_dead) {
m->destruct();
return false;
}
lock();
}
ExtModChan* chan = 0;
if ((m_role == RoleChannel) && !m_chan && m_setdata && (*m == "call.execute")) {
// we delayed channel creation as there was nothing to ref() it
chan = new ExtModChan(this);
m_chan = chan;
m->setParam("id",chan->id());
}
if (m_setdata)
m->userData(m_chan);
// now the newly created channel is referenced by the message
if (chan)
chan->deref();
id = m->id();
if (id && !chan) {
// Copy the user data pointer from waiting message with same id
ObjList *p = &m_waiting;
for (; p; p=p->next()) {
MsgHolder *h = static_cast<MsgHolder *>(p->get());
if (h && (h->m_id == id)) {
RefObject* ud = h->m_msg.userData();
Debug("ExtModReceiver",DebugAll,"Copying data pointer %p from %p '%s' [%p]",
ud,h->msg(),h->msg()->c_str(),this);
m->userData(ud);
break;
}
}
}
if (m_settime || !m->msgTime())
m->msgTime() = Time::now();
m->startup(this);
unlock();
return false;
}
m->destruct();
}
reportError(line);
return false;
}
void ExtModReceiver::describe(String& rval) const
{
rval << "\t";
switch (m_role) {
case RoleUnknown:
rval << "Unknown";
break;
case RoleGlobal:
rval << "Global";
break;
case RoleChannel:
rval << "Channel";
break;
default:
rval << "Invalid";
break;
}
if (m_dead)
rval << ", dead, use=" << m_use;
if (m_chan)
rval << ", has channel";
if (m_restart)
rval << ", autorestart";
if (m_pid > 0)
rval << ", pid=" << m_pid;
rval << "\r\n";
}
bool ExtModHandler::received(Message& msg)
{
String dest(msg.getValue("callto"));
if (dest.null())
return false;
static const Regexp r("^external/\\([^/]*\\)/\\([^ ]*\\)\\(.*\\)$");
if (!dest.matches(r))
return false;
CallEndpoint* ch = YOBJECT(CallEndpoint,msg.userData());
String t = dest.matchString(1);
int typ = 0;
if (t == "nochan")
typ = ExtModChan::NoChannel;
else if (t == "nodata")
typ = ExtModChan::DataNone;
else if (t == "play")
typ = ExtModChan::DataRead;
else if (t == "record")
typ = ExtModChan::DataWrite;
else if (t == "playrec")
typ = ExtModChan::DataBoth;
else {
Debug(DebugConf,"Invalid ExtModule method '%s', use 'nochan', 'nodata', 'play', 'record' or 'playrec'",
t.c_str());
return false;
}
if (typ == ExtModChan::NoChannel) {
ExtModReceiver *r = ExtModReceiver::build(dest.matchString(2).c_str(),
dest.matchString(3).trimBlanks().c_str(),
true);
if (!r)
return false;
bool ok = r->received(msg,1);
r->unuse();
return ok;
}
ExtModChan *em = ExtModChan::build(dest.matchString(2).c_str(),
dest.matchString(3).c_str(),typ);
if (!em) {
Debug(DebugCrit,"Failed to create ExtMod for '%s'",dest.matchString(2).c_str());
return false;
}
ExtModReceiver* recv = em->receiver();
// new messages must be blocked until connect() returns (if applicable)
if (ch)
em->waitMsg(&msg);
if (!(recv && recv->received(msg,1))) {
em->waitMsg(0);
int level = DebugWarn;
if (msg.getValue("error") || msg.getValue("reason"))
level = DebugNote;
Debug(level,"ExtMod '%s' did not handle call message",dest.matchString(2).c_str());
em->waiting(false);
if (recv)
recv->unuse();
em->deref();
return false;
}
recv->unuse();
if (ch) {
em->waitMsg(0);
ch->connect(em,msg.getValue("reason"));
em->waiting(false);
}
em->deref();
return true;
}
bool ExtModCommand::received(Message& msg)
{
String line(msg.getValue("line"));
if (!line.startsWith("external",true))
return complete(msg.getValue("partline"),msg.getValue("partword"),msg.retValue());;
line >> "external";
line.trimBlanks();
if (line.null() || line == "info") {
msg.retValue() = "";
int n = 0;
Lock lock(s_mutex);
ObjList *l = &s_modules;
for (; l; l=l->next()) {
ExtModReceiver *r = static_cast<ExtModReceiver *>(l->get());
if (r) {
msg.retValue() << ++n << ". " << r->scriptFile() << " " << r->commandArg() << "\r\n";
if (line)
r->describe(msg.retValue());
}
}
return true;
}
int blank = line.find(' ');
bool start = line.startSkip("start");
bool restart = start || line.startSkip("restart");
if (restart || line.startSkip("stop")) {
if (line.null())
return false;
blank = line.find(' ');
String arg;
if (blank >= 0)
arg = line.substr(blank+1);
ExtModReceiver *r = ExtModReceiver::find(line.substr(0,blank),arg);
if (r) {
if (start) {
msg.retValue() = "External already running\r\n";
return true;
}
else {
r->setRestart(false);
r->die();
msg.retValue() = "External command stopped\r\n";
}
}
else
msg.retValue() = "External not running\r\n";
if (!restart)
return true;
}
else if (line.startSkip("execute")) {
if (line.null())
return false;
blank = line.find(' ');
String exe = line.substr(0,blank);
adjustPath(exe);
if (blank >= 0)
line = line.substr(blank+1);
else
line.clear();
bool ok = runProgram(exe,line);
msg.retValue() = ok ? "External exec attempt\r\n" : "External exec failed\r\n";
return true;
}
ExtModReceiver *r = ExtModReceiver::build(line.substr(0,blank),
(blank >= 0) ? line.substr(blank+1).c_str() : (const char*)0);
msg.retValue() = r ? "External start attempt\r\n" : "External command failed\r\n";
return true;
}
bool ExtModCommand::complete(const String& partLine, const String& partWord, String& rval) const
{
if (partLine.null() && partWord.null())
return false;
if (partLine.null() || partLine == YSTRING("status") || partLine == YSTRING("help"))
Module::itemComplete(rval,"external",partWord);
else if (partLine == YSTRING("external")) {
for (const char** list = s_cmds; *list; list++)
Module::itemComplete(rval,*list,partWord);
return true;
}
else if (partLine == YSTRING("external restart") || partLine == YSTRING("external stop")) {
ObjList mod;
s_mutex.lock();
ObjList *l = &s_modules;
for (; l; l=l->next()) {
ExtModReceiver *r = static_cast<ExtModReceiver *>(l->get());
if (!r)
continue;
if (mod.find(r->scriptFile()))
continue;
mod.append(new String(r->scriptFile()));
}
s_mutex.unlock();
for (l = mod.skipNull(); l; l = l->skipNext())
Module::itemComplete(rval,l->get()->toString(),partWord);
}
else if (partLine.startsWith("external ")) {
String scr = partLine.substr(9);
if (!(scr.startSkip("restart") || scr.startSkip("stop")))
return false;
if (scr.null() || (scr.find(' ') >= 0))
return false;
ObjList arg;
s_mutex.lock();
ObjList *l = &s_modules;
for (; l; l=l->next()) {
ExtModReceiver *r = static_cast<ExtModReceiver *>(l->get());
if (!r || r->commandArg().null() || (r->scriptFile() != scr))
continue;
if (arg.find(r->commandArg()))
continue;
arg.append(new String(r->commandArg()));
}
s_mutex.unlock();
for (l = arg.skipNull(); l; l = l->skipNext())
Module::itemComplete(rval,l->get()->toString(),partWord);
}
return false;
}
bool ExtModStatus::received(Message& msg)
{
const String& dest = msg[YSTRING("module")];
if (dest && (dest != YSTRING("external")))
return false;
s_mutex.lock();
msg.retValue() << "name=" << __plugin.name()
<< ",type=misc;scripts=" << s_modules.count()
<< ",chans=" << s_chans.count() << "\r\n";
s_mutex.unlock();
return !dest.null();
}
bool ExtModHelp::received(Message& msg)
{
const String& line = msg[YSTRING("line")];
if (line && (line != YSTRING("external")))
return false;
msg.retValue() << " " << s_helpExternalCmd << "\r\n";
if (line)
msg.retValue() << s_helpExternalInfo << "\r\n";
return !line.null();
}
ExtListener::ExtListener(const char* name)
: Thread("ExtMod Listener"),
m_name(name), m_role(ExtModReceiver::RoleUnknown)
{
}
bool ExtListener::init(const NamedList& sect)
{
String role(sect.getValue("role"));
if (role == "global")
m_role = ExtModReceiver::RoleGlobal;
else if (role == "channel")
m_role = ExtModReceiver::RoleChannel;
else if (role) {
Debug(DebugConf,"Unknown role '%s' of listener '%s'",role.c_str(),m_name.c_str());
return false;
}
String type(sect.getValue("type"));
SocketAddr addr;
if (type.null())
return false;
else if (type == "unix") {
String path(sect.getValue("path"));
if (path.null() || !addr.assign(AF_UNIX) || !addr.host(path))
return false;
File::remove(path);
}
else if (type == "tcp") {
String host(sect.getValue("addr","127.0.0.1"));
int port = sect.getIntValue("port");
if (host.null() || !port || !addr.assign(AF_INET)
|| !addr.host(host) || !addr.port(port))
return false;
}
else {
Debug(DebugConf,"Unknown type '%s' of listener '%s'",type.c_str(),m_name.c_str());
return false;
}
if (!m_socket.create(addr.family(),SOCK_STREAM)) {
Debug(DebugWarn,"Could not create socket for listener '%s' error %d: %s",
m_name.c_str(),m_socket.error(),strerror(m_socket.error()));
return false;
}
m_socket.setReuse();
if (!m_socket.bind(addr)) {
Debug(DebugWarn,"Could not bind listener '%s' error %d: %s",
m_name.c_str(),m_socket.error(),strerror(m_socket.error()));
return false;
}
if (!m_socket.setBlocking(false) || !m_socket.listen())
return false;
return startup();
}
void ExtListener::run()
{
SocketAddr addr;
for (;;) {
Thread::idle();
if (Thread::check(false))
break;
Socket* skt = m_socket.accept(addr);
if (!skt) {
if (m_socket.canRetry())
continue;
Alarm("extmodule","socket",DebugWarn,"Error on accept(), shutting down ExtListener '%s'",m_name.c_str());
break;
}
String tmp = addr.host();
if (addr.port())
tmp << ":" << addr.port();
Debug(DebugInfo,"Listener '%s' got connection from '%s'",m_name.c_str(),tmp.c_str());
switch (m_role) {
case ExtModReceiver::RoleUnknown:
case ExtModReceiver::RoleGlobal:
case ExtModReceiver::RoleChannel:
ExtModReceiver::build(m_name,skt,0,m_role,tmp);
break;
default:
Debug(DebugWarn,"Listener '%s' hit invalid role %d",m_name.c_str(),m_role);
delete skt;
}
}
}
ExtListener* ExtListener::build(const char* name, const NamedList& sect)
{
if (null(name))
return 0;
ExtListener* ext = new ExtListener(name);
if (!ext->init(sect)) {
Alarm("extmodule","config",DebugWarn,"Could not start listener '%s'",name);
delete ext;
ext = 0;
}
return ext;
}
ExtModulePlugin::ExtModulePlugin()
: Plugin("extmodule"),
m_handler(0)
{
Output("Loaded module ExtModule");
}
ExtModulePlugin::~ExtModulePlugin()
{
Output("Unloading module ExtModule");
s_mutex.lock();
s_pluginSafe = false;
s_modules.clear();
// the receivers destroyed above should also clear chans but better be sure
s_chans.clear();
s_mutex.unlock();
}
bool ExtModulePlugin::isBusy() const
{
Lock lock(s_mutex);
return (s_chans.count() != 0);
}
void ExtModulePlugin::initialize()
{
Output("Initializing module ExtModule");
s_cfg = Engine::configFile("extmodule");
s_cfg.load();
s_maxQueue = s_cfg.getIntValue("general","maxqueue",DEF_MAXQUEUE,0,MAX_MAXQUEUE);
s_timeout = s_cfg.getIntValue("general","timeout",MSG_TIMEOUT);
s_timebomb = s_cfg.getBoolValue("general","timebomb",false);
s_settime = s_cfg.getBoolValue("general","settime",false);
s_trackName = s_cfg.getBoolValue("general","trackparam",false) ?
name().c_str() : (const char*)0;
int wf = s_cfg.getIntValue("general","waitflush",WAIT_FLUSH);
if (wf < 1)
wf = 1;
else if (wf > 100)
wf = 100;
s_waitFlush = wf;
if (!m_handler) {
m_handler = new ExtModHandler("call.execute",s_cfg.getIntValue("general","priority",100));
Engine::install(m_handler);
Engine::install(new ExtModCommand);
Engine::install(new ExtModStatus);
Engine::install(new ExtModHelp);
NamedList *sect = 0;
int n = s_cfg.sections();
for (int i = 0; i < n; i++) {
sect = s_cfg.getSection(i);
if (!sect)
continue;
String s(*sect);
if (s.startSkip("listener",true) && s)
ExtListener::build(s,*sect);
}
// start any scripts only after the listeners
sect = s_cfg.getSection("scripts");
if (sect) {
unsigned int len = sect->length();
for (unsigned int i=0; i<len; i++) {
NamedString *n = sect->getParam(i);
if (n) {
String arg = *n;
Engine::runParams().replaceParams(arg);
ExtModReceiver::build(n->name(),arg);
}
}
}
// and now start additional programs
sect = s_cfg.getSection("execute");
if (sect) {
unsigned int len = sect->length();
for (unsigned int i=0; i<len; i++) {
NamedString *n = sect->getParam(i);
if (n) {
String tmp = n->name();
String arg = *n;
adjustPath(tmp);
Engine::runParams().replaceParams(arg);
if (tmp)
runProgram(tmp,arg);
}
}
}
}
}
}; // anonymous namespace
/* vi: set ts=8 sw=4 sts=4 noet: */