Version bump for 2.3 release.
[irker.git] / irkerd
diff --git a/irkerd b/irkerd
index 0453a0a284ae71c73696d5228edb1bdd6fe07788..8dedeba7fcd4e88c541bdb1a2fd97fe5ad38736c 100755 (executable)
--- a/irkerd
+++ b/irkerd
@@ -43,7 +43,7 @@ CONNECTION_MAX = 200          # To avoid hitting a thread limit
 
 # No user-serviceable parts below this line
 
-version = "2.1"
+version = "2.3"
 
 import sys, getopt, urlparse, time, random, socket, signal, re
 import threading, Queue, SocketServer, select
@@ -136,7 +136,6 @@ class IRCClient():
                     (insocks, _o, _e) = select.select(sockets, [], [], timeout)
                     for s in insocks:
                         connmap[s.fileno()].consume()
-
                 else:
                     time.sleep(timeout)
 
@@ -339,7 +338,6 @@ class IRCServerConnection():
         self.ship("PRIVMSG %s :%s" % (target, text))
 
     def quit(self, message=""):
-        # Triggers an error that forces a disconnect.
         self.ship("QUIT" + (message and (" :" + message)))
 
     def user(self, username, realname):
@@ -430,7 +428,7 @@ class Connection:
         for (channel, message, key) in qcopy:
             self.queue.put((channel, message, key))
         self.status = "ready"
-    def enqueue(self, channel, message, key):
+    def enqueue(self, channel, message, key, quit_after):
         "Enque a message for transmission."
         if self.thread is None or not self.thread.is_alive():
             self.status = "unseen"
@@ -438,6 +436,8 @@ class Connection:
             self.thread.setDaemon(True)
             self.thread.start()
         self.queue.put((channel, message, key))
+        if quit_after:
+            self.queue.put((channel, None, key))
     def dequeue(self):
         "Try to ship pending messages from the queue."
         try:
@@ -524,10 +524,13 @@ class Connection:
                     if channel not in self.channels_joined:
                         self.connection.join(channel, key=key)
                         self.irker.irc.debug(1, "joining %s on %s." % (channel, self.servername))
+                    # None is magic - it's a request to quit the server
+                    if message is None:
+                        self.connection.quit()
                     # An empty message might be used as a keepalive or
                     # to join a channel for logging, so suppress the
                     # privmsg send unless there is actual traffic.
-                    if message:
+                    elif message:
                         for segment in message.split("\n"):
                             # Truncate the message if it's too long,
                             # but we're working with characters here,
@@ -540,6 +543,7 @@ class Connection:
                                 self.connection.privmsg(channel, segment)
                             except ValueError as err:
                                 self.irker.irc.debug(1, "irclib rejected a message to %s on %s because: %s" % (channel, self.servername, str(err)))
+                                self.irker.irc.debug(50, err.format_exc())
                             time.sleep(ANTI_FLOOD_DELAY)
                     self.last_xmit = self.channels_joined[channel] = time.time()
                     self.irker.irc.debug(1, "XMIT_TTL bump (%s transmission) at %s" % (self.servername, time.asctime()))
@@ -626,7 +630,7 @@ class Dispatcher:
         self.servername = servername
         self.port = port
         self.connections = []
-    def dispatch(self, channel, message, key):
+    def dispatch(self, channel, message, key, quit_after=False):
         "Dispatch messages for our server-port combination."
         # First, check if there is room for another channel
         # on any of our existing connections.
@@ -649,18 +653,21 @@ class Dispatcher:
             found_connection.part(drop_channel, "scavenged by irkerd")
             del found_connection.channels_joined[drop_channel]
             #time.sleep(ANTI_FLOOD_DELAY)
-            found_connection.enqueue(channel, message, key)
+            found_connection.enqueue(channel, message, key, quit_after)
             return
         # Didn't find any channels with no recent activity
         newconn = Connection(self.irker,
                              self.servername,
                              self.port)
         self.connections.append(newconn)
-        newconn.enqueue(channel, message, key)
+        newconn.enqueue(channel, message, key, quit_after)
     def live(self):
         "Does this server-port combination have any live connections?"
         self.connections = [x for x in self.connections if x.live()]
         return len(self.connections) > 0
+    def pending(self):
+        "Return all connections with pending traffic."
+        return [x for x in self.connections if not x.queue.empty()]
     def last_xmit(self):
         "Return the time of the most recent transmission."
         return max(x.last_xmit for x in self.connections)
@@ -680,11 +687,12 @@ class Irker:
         self.irc.add_event_handler("disconnect", self._handle_disconnect)
         self.irc.add_event_handler("kick", self._handle_kick)
         self.irc.add_event_handler("every_raw_message", self._handle_every_raw_message)
+        self.servers = {}
+    def thread_launch(self):
         thread = threading.Thread(target=self.irc.spin)
         thread.setDaemon(True)
         self.irc._thread = thread
         thread.start()
-        self.servers = {}
     def logerr(self, errmsg):
         "Log a processing error."
         sys.stderr.write("irkerd: " + errmsg + "\n")
@@ -745,7 +753,10 @@ class Irker:
             with open(logfile, "a") as logfp:
                 logfp.write("%03f|%s|%s\n" % \
                              (time.time(), event.source, event.arguments[0]))
-    def handle(self, line):
+    def pending(self):
+        "Do we have any pending message traffic?"
+        return [k for (k, v) in self.servers.items() if v.pending()]
+    def handle(self, line, quit_after=False):
         "Perform a JSON relay request."
         try:
             request = json.loads(line.strip())
@@ -772,7 +783,7 @@ class Irker:
                                 return
                             if target.server() not in self.servers:
                                 self.servers[target.server()] = Dispatcher(self, target.servername, target.port)
-                            self.servers[target.server()].dispatch(target.channel, message, target.key)
+                            self.servers[target.server()].dispatch(target.channel, message, target.key, quit_after=quit_after)
                             # GC dispatchers with no active connections
                             servernames = self.servers.keys()
                             for servername in servernames:
@@ -817,24 +828,26 @@ class IrkerUDPHandler(SocketServer.BaseRequestHandler):
 def usage():
     sys.stdout.write("""
 Usage:
-  irkerd [-d debuglevel] [-l logfile] [-n nick] [-p password] [-V] [-h]
+  irkerd [-d debuglevel] [-l logfile] [-n nick] [-p password] [-i channel message] [-V] [-h]
 
 Options
   -d    set debug level
   -l    set logfile
   -n    set nick-style
   -p    set nickserv password
+  -i    immediate mode
   -V    return irkerd version
   -h    print this help dialog
 """)
 
 if __name__ == '__main__':
     debuglvl = 0
+    immediate = None
     namestyle = "irker%03d"
     password = None
     logfile = None
     try:
-        (options, arguments) = getopt.getopt(sys.argv[1:], "d:l:n:p:Vh")
+        (options, arguments) = getopt.getopt(sys.argv[1:], "d:i:l:n:p:Vh")
     except getopt.GetoptError as e:
         sys.stderr.write("%s" % e)
         usage()
@@ -842,6 +855,8 @@ if __name__ == '__main__':
     for (opt, val) in options:
         if opt == '-d':                # Enable debug/progress messages
             debuglvl = int(val)
+        elif opt == '-i':      # Immediate mode - send one message, then exit. 
+            immediate = val
         elif opt == '-l':      # Logfile mode - report traffic read in
             logfile = val
         elif opt == '-n':      # Force the nick
@@ -857,18 +872,26 @@ if __name__ == '__main__':
     fallback = re.search("%.*d", namestyle)
     irker = Irker(debuglevel=debuglvl)
     irker.irc.debug(1, "irkerd version %s" % version)
-    try:
-        tcpserver = SocketServer.TCPServer((HOST, PORT), IrkerTCPHandler)
-        udpserver = SocketServer.UDPServer((HOST, PORT), IrkerUDPHandler)
-        for server in [tcpserver, udpserver]:
-            server = threading.Thread(target=server.serve_forever)
-            server.setDaemon(True)
-            server.start()
+    if immediate:
+        def bailout():
+            raise SystemExit, 1
+        irker.irc.add_event_handler("quit", lambda _c, _e: bailout())
+        irker.handle('{"to":"%s","privmsg":"%s"}' % (immediate, arguments[0]), quit_after=True)
+        irker.irc.spin()
+    else:
+        irker.thread_launch()
         try:
-            signal.pause()
-        except KeyboardInterrupt:
-            raise SystemExit(1)
-    except socket.error, e:
-        sys.stderr.write("irkerd: server launch failed: %r\n" % e)
+            tcpserver = SocketServer.TCPServer((HOST, PORT), IrkerTCPHandler)
+            udpserver = SocketServer.UDPServer((HOST, PORT), IrkerUDPHandler)
+            for server in [tcpserver, udpserver]:
+                server = threading.Thread(target=server.serve_forever)
+                server.setDaemon(True)
+                server.start()
+            try:
+                signal.pause()
+            except KeyboardInterrupt:
+                raise SystemExit(1)
+        except socket.error, e:
+            sys.stderr.write("irkerd: server launch failed: %r\n" % e)
 
 # end