From eec024bf9a3af03b31449f3c23c87b8711dbdda1 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 9 Jul 2015 18:29:00 -0700 Subject: [PATCH 1/4] Second attempt at multinet support, by wrapping threading.Thread around Irc.run() Closes #15. --- main.py | 55 +++++++++++++++++++++++++++++++++---------------------- utils.py | 1 + 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/main.py b/main.py index cc6475b..526d9b2 100755 --- a/main.py +++ b/main.py @@ -6,16 +6,19 @@ import socket import time import sys from collections import defaultdict +import threading from log import log import conf import classes +import utils class Irc(): - def __init__(self, proto, conf): + def __init__(self, netname, proto, conf): + # threading.Thread.__init__(self) # Initialize some variables self.connected = False - self.name = conf['server']['netname'] + self.name = netname self.conf = conf # Server, channel, and user indexes to be populated by our protocol module self.servers = {} @@ -38,7 +41,7 @@ class Irc(): self.maxnicklen = 30 self.prefixmodes = 'ov' - self.serverdata = conf['server'] + self.serverdata = conf['servers'][netname] self.sid = self.serverdata["sid"] self.botdata = conf['bot'] self.proto = proto @@ -49,12 +52,20 @@ class Irc(): port = self.serverdata["port"] log.info("Connecting to network %r on %s:%s", self.name, ip, port) self.socket = socket.socket() + self.socket.setblocking(0) + self.socket.settimeout(60) self.socket.connect((ip, port)) self.proto.connect(self) self.loaded = [] self.load_plugins() + reading_thread = threading.Thread(target = self.run) self.connected = True - self.run() + reading_thread.start() + + def disconnect(self): + self.connected = False + self.socket.shutdown() + self.socket.close() def run(self): buf = "" @@ -67,19 +78,19 @@ class Irc(): break while '\n' in buf: line, buf = buf.split('\n', 1) - log.debug("<- %s", line) + log.debug("(%s) <- %s", self.name, line) proto.handle_events(self, line) - except socket.error as e: - log.error('Received socket.error: %s, exiting.', str(e)) - break - sys.exit(1) + except (socket.error, classes.ProtocolError) as e: + log.error('Disconnected from network %r: %s: %s, exiting.', + self.name, type(e).__name__, str(e)) + self.disconnect() def send(self, data): # Safeguard against newlines in input!! Otherwise, each line gets # treated as a separate command, which is particularly nasty. data = data.replace('\n', ' ') data = data.encode("utf-8") + b"\n" - log.debug("-> %s", data.decode("utf-8").strip("\n")) + log.debug("(%s) -> %s", self.name, data.decode("utf-8").strip("\n")) self.socket.send(data) def load_plugins(self): @@ -103,17 +114,17 @@ if __name__ == '__main__': if conf.conf['login']['password'] == 'changeme': log.critical("You have not set the login details correctly! Exiting...") sys.exit(2) - - protoname = conf.conf['server']['protocol'] protocols_folder = [os.path.join(os.getcwd(), 'protocols')] - try: - moduleinfo = imp.find_module(protoname, protocols_folder) - proto = imp.load_source(protoname, moduleinfo[1]) - except ImportError as e: - if str(e).startswith('No module named'): - log.critical('Failed to load protocol module %r: the file could not be found.', protoname) + for network in conf.conf['servers']: + protoname = conf.conf['servers'][network]['protocol'] + try: + moduleinfo = imp.find_module(protoname, protocols_folder) + proto = imp.load_source(protoname, moduleinfo[1]) + except ImportError as e: + if str(e).startswith('No module named'): + log.critical('Failed to load protocol module %r: the file could not be found.', protoname) + else: + log.critical('Failed to load protocol module: import error %s', protoname, str(e)) + sys.exit(2) else: - log.critical('Failed to load protocol module: import error %s', protoname, str(e)) - sys.exit(2) - else: - irc_obj = Irc(proto, conf.conf) + utils.networkobjects[network] = Irc(network, proto, conf.conf) diff --git a/utils.py b/utils.py index 1f1746e..aa34d57 100644 --- a/utils.py +++ b/utils.py @@ -8,6 +8,7 @@ global bot_commands, command_hooks # This should be a mapping of command names to functions bot_commands = {} command_hooks = defaultdict(list) +networkobjects = {} class TS6UIDGenerator(): """TS6 UID Generator module, adapted from InspIRCd source From 652d53c29e2232539e78c390fed3830f883959c3 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 9 Jul 2015 18:40:32 -0700 Subject: [PATCH 2/4] pr/insp: drop tests for validing channel names --- tests/test_proto_inspircd.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_proto_inspircd.py b/tests/test_proto_inspircd.py index aa551de..f17ed7c 100644 --- a/tests/test_proto_inspircd.py +++ b/tests/test_proto_inspircd.py @@ -56,8 +56,6 @@ class TestProtoInspIRCd(unittest.TestCase): self.assertIn(u, self.irc.channels['#channel'].users) # Non-existant user. self.assertRaises(LookupError, self.proto.joinClient, self.irc, '9PYZZZZZZ', '#test') - # Invalid channel. - self.assertRaises(ValueError, self.proto.joinClient, self.irc, u, 'aaaa') def testPartClient(self): u = self.u From ffec80ee45e2ca5c037d1828728e08ca124053e5 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 9 Jul 2015 18:42:38 -0700 Subject: [PATCH 3/4] tests/: update for new multi-net config format --- classes.py | 23 ++++++++++++----------- tests/test_fakeirc.py | 2 +- tests/test_proto_inspircd.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/classes.py b/classes.py index 8e17667..3a035d6 100644 --- a/classes.py +++ b/classes.py @@ -67,17 +67,18 @@ testconf = {'bot': 'realname': 'PyLink Service Client', 'loglevel': 'DEBUG', }, - 'server': - { - 'netname': 'fakeirc', - 'ip': '0.0.0.0', - 'port': 7000, - 'recvpass': "abcd", - 'sendpass': "abcd", - 'protocol': "null", - 'hostname': "pylink.unittest", - 'sid': "9PY", - 'channels': ["#pylink"], + 'servers': + {'unittest': + { + 'ip': '0.0.0.0', + 'port': 7000, + 'recvpass': "abcd", + 'sendpass': "abcd", + 'protocol': "null", + 'hostname': "pylink.unittest", + 'sid': "9PY", + 'channels': ["#pylink"], + }, }, } diff --git a/tests/test_fakeirc.py b/tests/test_fakeirc.py index 4a640a4..95bc800 100644 --- a/tests/test_fakeirc.py +++ b/tests/test_fakeirc.py @@ -9,7 +9,7 @@ import unittest # Yes, we're going to even test the testing classes. Testception? I think so. class TestFakeIRC(unittest.TestCase): def setUp(self): - self.irc = classes.FakeIRC(classes.FakeProto(), classes.testconf) + self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf) def testFakeIRC(self): self.irc.run('this should do nothing') diff --git a/tests/test_proto_inspircd.py b/tests/test_proto_inspircd.py index f17ed7c..5f9eb6c 100644 --- a/tests/test_proto_inspircd.py +++ b/tests/test_proto_inspircd.py @@ -11,7 +11,7 @@ import utils class TestProtoInspIRCd(unittest.TestCase): def setUp(self): - self.irc = classes.FakeIRC(inspircd, classes.testconf) + self.irc = classes.FakeIRC('unittest', inspircd, classes.testconf) self.proto = self.irc.proto self.sdata = self.irc.serverdata # This is to initialize ourself as an internal PseudoServer, so we can spawn clients From eae0aa5aa857656e58ab267d219814c0b3279854 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 9 Jul 2015 19:05:37 -0700 Subject: [PATCH 4/4] pr/insp: Add missing killServer/killClient functions --- protocols/inspircd.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index bbefd2a..93b237c 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -150,9 +150,31 @@ def modeServer(irc, numeric, target, modes): a list of (mode, arg) tuples, in the format of utils.parseModes() output. """ if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') + raise LookupError('No such PyLink PseudoServer exists.') _sendModes(irc, numeric, target, modes) +def killServer(irc, numeric, target, reason): + """ + + Sends a kill to from a PyLink PseudoServer. + """ + if not utils.isInternalServer(irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + _sendFromServer(irc, numeric, 'KILL %s :%s' % (target, reason)) + # We don't need to call removeClient here, since the remote server + # will send a QUIT from the target if the command succeeds. + +def killClient(irc, numeric, target, reason): + """ + + Sends a kill to from a PyLink PseudoClient. + """ + if not utils.isInternalClient(irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + _sendFromServer(irc, numeric, 'KILL %s :%s' % (target, reason)) + # We don't need to call removeClient here, since the remote server + # will send a QUIT from the target if the command succeeds. + def messageClient(irc, numeric, target, text): """