/** * 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-2006 Null Team * Portions copyright (C) 2005 Maciek Kaminski * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #ifdef _WINDOWS #include #else #include #include #include #endif #include #include #include #include #include using namespace TelEngine; namespace { // anonymous // Maximum length of an incoming line #define MAX_INCOMING_LINE 8192 // Default message timeout in milliseconds #define MSG_TIMEOUT 10000 static Configuration s_cfg; static ObjList s_chans; static ObjList s_modules; static Mutex s_mutex(true); static int s_timeout = MSG_TIMEOUT; static bool s_timebomb = false; 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 void Consume(const DataBlock& data, unsigned long timestamp); 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); } private: ExtModChan(const char* file, const char* args, int type); ExtModReceiver *m_recv; int m_type; bool m_running; bool m_disconn; }; // Great idea - thanks, Maciek! class ExtMessage : public Message { YCLASS(ExtMessage,Message) public: inline ExtMessage(ExtModReceiver* recv) : Message(""), m_receiver(recv) { } void startup(); 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; }; class MsgHolder : public GenObject { public: MsgHolder(Message &msg); Message &m_msg; bool m_ret; String m_id; bool decode(const char *s); }; // Yet Another of Maciek's ideas class MsgWatcher : public MessagePostHook { public: inline MsgWatcher(ExtModReceiver* receiver) : m_receiver(receiver) { } virtual ~MsgWatcher(); virtual void dispatched(const Message& msg, bool handled); bool addWatched(const String& name); bool delWatched(const String& name); private: ExtModReceiver* m_receiver; ObjList m_watched; }; class ExtModReceiver : public MessageReceiver, public Mutex { friend class MsgWatcher; public: enum { RoleUnknown, RoleGlobal, RoleChannel }; static ExtModReceiver* build(const char *script, const char *args, File* ain = 0, File* aout = 0, ExtModChan *chan = 0); static ExtModReceiver* build(const char* name, Stream* io, ExtModChan* chan = 0, int role = RoleUnknown); static ExtModReceiver* find(const String& script); ~ExtModReceiver(); 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); inline void use() { m_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; } private: ExtModReceiver(const char* script, const char* args, File* ain, File* aout, ExtModChan* chan); ExtModReceiver(const char* name, Stream* io, ExtModChan* chan, int role); bool create(const char* script, const char* args); void closeIn(); void closeOut(); void closeAudio(); int m_role; bool m_dead; int m_use; 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; int m_timeout; bool m_timebomb; String m_script, m_args; ObjList m_waiting; ObjList m_relays; String m_reason; }; class ExtThread : public Thread { public: ExtThread(ExtModReceiver* receiver) : Thread("ExtModule"), m_receiver(receiver) { } virtual void run() { m_receiver->run(); } virtual void cleanup() { m_receiver->cleanup(); } private: ExtModReceiver* m_receiver; }; class ExtModHandler : public MessageHandler { public: ExtModHandler(const char *name) : MessageHandler(name) { } virtual bool received(Message &msg); }; class ExtModCommand : public MessageHandler { public: ExtModCommand(const char *name) : MessageHandler(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; }; class ExtModulePlugin : public Plugin { public: ExtModulePlugin(); ~ExtModulePlugin(); virtual void initialize(); virtual bool isBusy() const; private: ExtModHandler *m_handler; }; 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; } 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("ExtModSource"); } } 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 = 0; u_int64_t tpos = Time::now(); do { 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); } while (r > 0); 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; } } void ExtModConsumer::Consume(const DataBlock& data, unsigned long timestamp) { if ((m_str) && !data.null()) { m_str->writeData(data); m_total += data.length(); } } 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_type(type), m_disconn(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,reader,writer,this); } ExtModChan::ExtModChan(ExtModReceiver* recv) : CallEndpoint("ExtModule"), m_recv(recv), m_type(DataNone), m_disconn(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); s_mutex.unlock(); setSource(); setConsumer(); if (m_recv) m_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); if (getPeer()) m->addParam("peerid",getPeer()->id()); 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()); m_id = buf; } bool MsgHolder::decode(const char *s) { return (m_msg.decode(s,m_ret,m_id) == -2); } void ExtMessage::startup() { if (m_receiver && m_id) m_receiver->use(); Engine::enqueue(this); } void ExtMessage::dispatched(bool accepted) { if (m_receiver && m_id) { m_receiver->returnMsg(this,m_id,accepted); m_receiver->unuse(); } } MsgWatcher::~MsgWatcher() { Engine::self()->setHook(this,true); if (m_receiver && (m_receiver->m_watcher == this)) m_receiver->m_watcher = 0; m_receiver = 0; } void MsgWatcher::dispatched(const Message& msg, bool handled) { if (!m_receiver) return; if (!m_receiver->selfWatch()) { // check if the message was generated by ourselves - avoid reentrance ExtMessage* m = YOBJECT(ExtMessage,&msg); if (m && m->belongsTo(m_receiver)) return; } ObjList* l = m_watched.skipNull(); for (; l; l = l->skipNext()) { const String* s = static_cast(l->get()); if (s->null() || (*s == msg)) { m_receiver->returnMsg(&msg,"",handled); break; } } } 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; } ExtModReceiver* ExtModReceiver::build(const char* script, const char* args, File* ain, File* aout, ExtModChan* chan) { ExtModReceiver* recv = new ExtModReceiver(script,args,ain,aout,chan); if (!recv->start()) { recv->destruct(); return 0; } return recv; } ExtModReceiver* ExtModReceiver::build(const char* name, Stream* io, ExtModChan* chan, int role) { ExtModReceiver* recv = new ExtModReceiver(name,io,chan,role); if (!recv->start()) { recv->destruct(); return 0; } return recv; } ExtModReceiver* ExtModReceiver::find(const String& script) { Lock lock(s_mutex); ObjList *l = &s_modules; for (; l; l=l->next()) { ExtModReceiver *r = static_cast(l->get()); if (r && (r->scriptFile() == script)) return r; } return 0; } bool ExtModReceiver::unuse() { int u = --m_use; if (!u) destruct(); return (u <= 0); } ExtModReceiver::ExtModReceiver(const char* script, const char* args, File* ain, File* aout, ExtModChan* chan) : Mutex(true), m_role(RoleUnknown), m_dead(false), m_use(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_timeout(s_timeout), m_timebomb(s_timebomb), m_script(script), m_args(args) { 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) : Mutex(true), m_role(role), m_dead(false), m_use(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_timeout(s_timeout), m_timebomb(s_timebomb), m_script(name) { Debug(DebugAll,"ExtModReceiver::ExtModReceiver(\"%s\",%p,%p) [%p]",name,io,chan,this); m_script.trimBlanks(); if (chan) m_role = RoleChannel; s_mutex.lock(); s_modules.append(this); s_mutex.unlock(); } ExtModReceiver::~ExtModReceiver() { Debug(DebugAll,"ExtModReceiver::~ExtModReceiver() [%p] pid=%d",this,m_pid); Lock lock(this); // One destruction is plenty enough m_use = -100; s_mutex.lock(); s_modules.remove(this,false); s_mutex.unlock(); die(); if (m_pid > 1) Debug(DebugWarn,"ExtModReceiver::~ExtModReceiver() [%p] pid=%d",this,m_pid); closeAudio(); } void ExtModReceiver::closeIn() { if (!m_in) return; Stream* tmp = m_in; m_in = 0; if (m_out == tmp) m_out = 0; if (tmp) delete tmp; } void ExtModReceiver::closeOut() { if (!m_out) return; Stream* tmp = m_out; m_out = 0; if (m_in == tmp) m_in = 0; if (tmp) delete tmp; } 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()) return false; while (m_pid < 0) Thread::yield(); } return (m_pid >= 0); } bool ExtModReceiver::flush() { if (m_watcher) { m_watcher->destruct(); m_watcher = 0; } // Make sure we release all pending messages and not accept new ones if (!Engine::exiting()) m_relays.clear(); else { ObjList *p = &m_relays; for (; p; p=p->next()) p->setDelete(false); } if (m_waiting.get()) { DDebug(DebugAll,"ExtModReceiver releasing pending messages [%p]",this); m_waiting.clear(); Thread::yield(); return true; } return false; } 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; m_dead = true; use(); flush(); ExtModChan *chan = m_chan; m_chan = 0; if (chan) chan->setRecv(0); // Give the external script a chance to die gracefully closeOut(); if (m_pid > 1) { Debug(DebugAll,"ExtModReceiver::die() waiting for pid=%d to die",m_pid); 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?",m_pid); // Now terminate the process and close its stdout pipe closeIn(); #ifndef _WINDOWS if (m_pid > 1) ::kill(m_pid,SIGTERM); #endif if (chan && clearChan) chan->disconnect(m_reason); unuse(); } bool ExtModReceiver::received(Message &msg, int id) { lock(); // check if we are no longer running bool ok = (m_pid > 0) && m_in && m_out; 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) { 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_waiting.append(&h)->setDelete(false); DDebug(DebugAll,"ExtMod [%p] queued message '%s' [%p]",this,msg.c_str(),&msg); } else { Debug(DebugWarn,"ExtMod [%p] could not queue message '%s'",this,msg.c_str()); 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) { Thread::yield(); lock(); ok = (m_waiting.find(&h) != 0); if (ok && tout && (Time::now() > tout)) { Debug(DebugWarn,"Message '%s' did not return in %d msec [%p]", msg.c_str(),m_timeout,this); m_waiting.remove(&h,false); ok = false; fail = true; } unlock(); } DDebug(DebugAll,"ExtMod [%p] message '%s' [%p] returning %s",this,msg.c_str(),&msg, h.m_ret ? "true" : "false"); 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; if (script[0] != '/') { tmp = s_cfg.getValue("general","scripts_dir",SCR_PATH); if (!tmp.endsWith(Engine::pathSeparator())) tmp += Engine::pathSeparator(); tmp += script; } 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); } Debug(DebugInfo,"Launched External Script %s", script); 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_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; } use(); if (m_in) m_in->setBlocking(false); char buffer[MAX_INCOMING_LINE]; int posinbuf = 0; DDebug(DebugAll,"ExtModReceiver::run() entering loop [%p]",this); for (;;) { use(); int readsize = m_in ? m_in->readData(buffer+posinbuf,sizeof(buffer)-posinbuf-1) : 0; XDebug(DebugAll,"ExtModReceiver::run() read %d",readsize); if (unuse()) return; if (!readsize) { lock(); if (m_in) Debug("ExtModule",DebugInfo,"Read EOF on %p [%p]",m_in,this); closeIn(); flush(); unlock(); if (m_chan && m_chan->running()) Thread::sleep(1); break; } else if (readsize < 0) { if (m_in && m_in->canRetry()) { Thread::msleep(5); continue; } Debug("ExtModule",DebugWarn,"Read error %d on %p [%p]",errno,m_in,this); break; } int totalsize = readsize + posinbuf; 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; if (buffer[0]) { use(); bool goOut = processLine(buffer); if (unuse() || goOut) return; } totalsize -= eoline-buffer+1; ::memmove(buffer,eoline+1,totalsize+1); } posinbuf = totalsize; } } bool ExtModReceiver::outputLine(const char* line) { DDebug("ExtModReceiver",DebugAll,"%soutputLine '%s'", (m_out ? "" : "failing "), line); if (!m_out) return false; m_out->writeData(line); char nl = '\n'; m_out->writeData(&nl,sizeof(nl)); return true; } 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)); lock(); outputLine(ret); unlock(); } bool ExtModReceiver::addWatched(const String& name) { Lock mylock(this); 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); return m_watcher && m_watcher->delWatched(name); } bool ExtModReceiver::processLine(const char* line) { 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("%%next()) { MsgHolder *msg = static_cast(p->get()); if (msg && msg->decode(line)) { DDebug("ExtModReceiver",DebugInfo,"Matched message"); p->remove(false); return false; } } Debug("ExtModReceiver",DebugWarn,"Unmatched message: %s",line); return false; } else if (id.startSkip("%%>install:",false)) { int prio = 100; id >> prio >> ":"; bool ok = true; String fname; String fvalue; Regexp r("^\\([^:]*\\):\\([^:]*\\):\\?\\(.*\\)"); if (id.matches(r)) { // a filter is specified fname = id.matchString(2); fvalue = id.matchString(3); id = id.matchString(1); } // sanity checks ok = ok && id && !m_relays.find(id); if (ok) { MessageRelay *r = new MessageRelay(id,this,0,prio); if (fname) r->setFilter(fname,fvalue); m_relays.append(r); Engine::install(r); } Debug("ExtModReceiver",DebugAll,"Install '%s', prio %d %s", id.c_str(),prio,ok ? "ok" : "failed"); String out("%%uninstall:",false)) { int prio = 0; bool ok = false; ObjList *p = &m_relays; for (; p; p=p->next()) { MessageRelay *r = static_cast(p->get()); if (r && (*r == id)) { prio = r->priority(); p->remove(); ok = true; break; } } Debug("ExtModReceiver",DebugAll,"Uninstall '%s' %s", id.c_str(),ok ? "ok" : "failed"); String out("%%watch:",false)) { bool ok = addWatched(id); Debug("ExtModReceiver",DebugAll,"Watch '%s' %s", id.c_str(),ok ? "ok" : "failed"); String out("%%unwatch:",false)) { bool ok = delWatched(id); Debug("ExtModReceiver",DebugAll,"Unwatch '%s' %s", id.c_str(),ok ? "ok" : "failed"); String out("%%output:",false)) { id.trimBlanks(); Output("%s",id.safe()); return false; } else if (id.startSkip("%%>setlocal:",false)) { int col = id.find(':'); if (col > 0) { String val(id.substr(col+1)); id = id.substr(0,col); bool ok = 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 == "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 == "reenter") { m_reenter = val.toBoolean(m_reenter); val = m_reenter; ok = true; } else if (id == "selfwatch") { m_selfWatch = val.toBoolean(m_selfWatch); val = m_selfWatch; ok = true; } DDebug("ExtModReceiver",DebugAll,"Set '%s'='%s' %s", id.c_str(),val.c_str(),ok ? "ok" : "failed"); String out("%%quit") { outputLine("%%decode(line) == -2) { DDebug("ExtModReceiver",DebugAll,"Created message %p '%s' [%p]",m,m->c_str(),this); lock(); ExtModChan* chan = 0; if ((m_role == RoleChannel) && !m_chan && (*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()); } m->userData(m_chan); // now the newly created channel is referenced by the message if (chan) chan->deref(); id = m->id(); if (id) { // Copy the user data pointer from waiting message with same id ObjList *p = &m_waiting; for (; p; p=p->next()) { MsgHolder *h = static_cast(p->get()); if (h && (h->m_id == id)) { RefObject* ud = h->m_msg.userData(); Debug("ExtModReceiver",DebugAll,"Copying data pointer %p from %p",ud,&(h->m_msg)); m->userData(ud); break; } } } m->startup(); unlock(); return false; } m->destruct(); } reportError(line); return false; } bool ExtModHandler::received(Message& msg) { String dest(msg.getValue("callto")); if (dest.null()) return false; Regexp r("^external/\\([^/]*\\)/\\([^ ]*\\)\\(.*\\)$"); if (!dest.matches(r)) return false; CallEndpoint* ch = static_cast(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(DebugGoOn,"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()); return r ? r->received(msg,1) : false; } ExtModChan *em = ExtModChan::build(dest.matchString(2).c_str(), dest.matchString(3).c_str(),typ); if (!em) { Debug(DebugGoOn,"Failed to create ExtMod for '%s'",dest.matchString(2).c_str()); return false; } if (!(em->receiver() && em->receiver()->received(msg,1))) { 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->deref(); return false; } if (ch) ch->connect(em); em->deref(); return true; } bool ExtModCommand::received(Message& msg) { String line(msg.getValue("line")); if (!line.startsWith("external",true)) return false; line >> "external"; line.trimBlanks(); if (line.null()) { msg.retValue() = ""; int n = 0; Lock lock(s_mutex); ObjList *l = &s_modules; for (; l; l=l->next()) { ExtModReceiver *r = static_cast(l->get()); if (r) msg.retValue() << ++n << ". " << r->scriptFile() << " " << r->commandArg() << "\n"; } return true; } int blank = line.find(' '); if (blank <= 0) { if (line == "stop") return false; ExtModReceiver *r = ExtModReceiver::build(line,(const char*)0); msg.retValue() = r ? "External start attempt\n" : "External command failed!\n"; return true; } if (line.startSkip("stop")) { if (line.null()) return false; ExtModReceiver *r = ExtModReceiver::find(line); if (r) { r->die(); msg.retValue() = "External command stopped\n"; } else msg.retValue() = "External not running\n"; return true; } ExtModReceiver *r = ExtModReceiver::build(line.substr(0,blank),line.substr(blank+1)); msg.retValue() = r ? "External start attempt\n" : "External command failed\n"; return true; } ExtListener::ExtListener(const char* name) : Thread("ExtListener"), 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(DebugWarn,"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(DebugWarn,"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; } 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::msleep(5,true); Socket* skt = m_socket.accept(addr); if (!skt) { if (m_socket.canRetry()) continue; Debug(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); 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; SocketAddr addr; ExtListener* ext = new ExtListener(name); if (!ext->init(sect)) { Debug(DebugGoOn,"Could not start listener '%s'",name); delete ext; ext = 0; } return ext; } ExtModulePlugin::ExtModulePlugin() : m_handler(0) { Output("Loaded module ExtModule"); } ExtModulePlugin::~ExtModulePlugin() { Output("Unloading module ExtModule"); s_mutex.lock(); 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_timeout = s_cfg.getIntValue("general","timeout",MSG_TIMEOUT); s_timebomb = s_cfg.getBoolValue("general","timebomb",false); if (!m_handler) { m_handler = new ExtModHandler("call.execute"); Engine::install(m_handler); Engine::install(new ExtModCommand("engine.command")); 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; igetParam(i); if (n) ExtModReceiver::build(n->name(),*n); } } // and now start additional programs sect = s_cfg.getSection("execute"); if (sect) { unsigned int len = sect->length(); for (unsigned int i=0; igetParam(i); if (n) runProgram(n->name(),*n); } } } } INIT_PLUGIN(ExtModulePlugin); }; // anonymous namespace /* vi: set ts=8 sw=4 sts=4 noet: */