From 4af78fa4aab07d24709a0bb9b56fd870aacf2666 Mon Sep 17 00:00:00 2001 From: Sebastien Helleu Date: Sun, 27 Oct 2013 16:58:46 +0100 Subject: [PATCH] weercd.py: use argparse module to parse command line arguments, remove config file The configuration file weercd.conf has been removed. Instead, default options can be set in an environment variable called "WEERCD_OPTIONS". A file with options can be used, then name must be given as option with a leading "@", for example: python weercd.py @args.txt The option "action" has been removed. Default behavior is still to flood the client. Actions "user" and "file" have been merged into a single option -f/--file, which accepts a file, or special value "-" to read stdin. The script now requires python >= 2.7 (because the argparse module is not available in python 2.6 and older versions). --- test/weercd.conf | 14 -- test/weercd.py | 341 +++++++++++++++++++++-------------------------- 2 files changed, 150 insertions(+), 205 deletions(-) delete mode 100644 test/weercd.conf diff --git a/test/weercd.conf b/test/weercd.conf deleted file mode 100644 index 68c51395c..000000000 --- a/test/weercd.conf +++ /dev/null @@ -1,14 +0,0 @@ -# -# weercd configuration -# -host= -port=7777 -debug=off -action=flood -wait=0 -sleep=0 -nickused=0 -maxchans=5 -maxnicks=100 -usernotices=on -channotices=on diff --git a/test/weercd.py b/test/weercd.py index 80db7fe8e..b64938f59 100755 --- a/test/weercd.py +++ b/test/weercd.py @@ -20,13 +20,15 @@ # # -# weercd - the WeeChat IRC server for testing purposes +# weercd - the WeeChat IRC testing server # -# weercd is an IRC server that is designed to test client resistance and memory -# usage (quickly detect memory leaks, for example with client scripts). -# Various IRC commands are sent in a short time (privmsg, notice, join/quit, ..) +# It can be used with any IRC client (not only WeeChat). # -# This script works with Python 2.x and 3.x. +# In the "flood" mode, various IRC commands are sent in a short time (privmsg, +# notice, join/quit, ..) to test client resistance and memory usage (to quickly +# detect memory leaks, for example with client scripts). +# +# This script works with Python 2.x (>= 2.7) and 3.x. # # It is *STRONGLY RECOMMENDED* to connect this server with a client in a test # environment: @@ -41,7 +43,7 @@ # python weercd.py # 2. open another terminal and run WeeChat with home in /tmp: # weechat --dir /tmp/weechat -# 3. optional: install script(s) in /tmp/weechat//autoload/ +# 3. optional: install script(s) (/script install ...) # 4. add server and connect to it: # /server add weercd 127.0.0.1/7777 # /connect weercd @@ -50,127 +52,53 @@ # Yeah, it's stable \o/ # -import sys, socket, select, time, random, string, re +import argparse +import os +import random +import re +import select +import shlex +import socket +import string +import sys +import time -NAME = 'weercd' -VERSION = '0.6' +NAME = 'weercd' +VERSION = '0.7' +DESCRIPTION = 'The WeeChat IRC testing server.' -options = { - 'host' : ['', 'Host for socket bind'], - 'port' : ['7777', 'Port for socket bind'], - 'debug' : ['off', 'Debug (on/off)'], - 'action' : ['flood', 'Action of server: "flood" = flood client, "user" = type messages to send to client, "file" = messages sent from a file'], - 'wait' : ['0', 'Time to wait before flooding client (float, in seconds)'], - 'sleep' : ['0', 'Sleep for select, delay between 2 messages sent to client (float, in seconds)'], - 'nickused' : ['0', 'Send 433 (nickname already in use) this number of times before accepting nick'], - 'maxchans' : ['5', 'Max channels to join'], - 'maxnicks' : ['100', 'Max nicks per channel'], - 'usernotices': ['on', 'Send notices to user (on/off)'], - 'channotices': ['on', 'Send notices to channels (on/off)'], - 'file' : ['', 'Filename used for sending messages to client (only for action "file")'], -} - -def usage(): - """Display usage.""" - global options - print('\nUsage: %s [option=value [option=value ...]] | [-h | --help]\n' % sys.argv[0]) - print(' option=value option with value (see table below)') - print(' -h, --help display this help\n') - print('Options (name, default value, description):\n') - for key in options: - v = '"%s"' % options[key][0] - print(' %-12s %-10s %s' % (key, v, options[key][1])) - print('\nOptions can be written in %s.conf, with format for each line: option=value\n' % NAME) - sys.exit(0) - -def setoption(string): - """Set option with string using format "option=value".""" - global options - items = string.strip().split('=', 1) - if len(items) == 2: - key = items[0].strip() - if not key.startswith('#'): - value = items[1].strip() - if key in options: - options[key][0] = value - else: - print('WARNING: unknown option "%s"' % key) - -def getoption(option): - """Get value of an option.""" - global options - if option in options: - return options[option][0] - return None - -def getdictoptions(): - """Get dict with options and values.""" - global options - d = {} - for key in options: - d[key] = options[key][0] - return d - -def readconfig(filename): - """Read configuration file.""" - try: - lines = open(filename, 'rb').readlines() - for line in lines: - setoption(str(line.decode('utf-8'))) - except: - pass - -def strrand(minlength=1, maxlength=50, spaces=False): - """Return string with random lenght and content.""" - length = random.randint(minlength, maxlength) - strspace = '' - if spaces: - strspace = ' ' - return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits + strspace) for x in range(length)) class Client: - def __init__(self, sock, addr, **kwargs): + def __init__(self, sock, addr, args, **kwargs): self.sock, self.addr = sock, addr - self.action = getoption('action') + self.args = args self.nick = '' self.nicknumber = 0 self.channels = {} self.lastbuf = '' self.incount, self.outcount, self.inbytes, self.outbytes = 0, 0, 0, 0 self.quit, self.endmsg, self.endexcept = False, '', None - self.sleep = float(getoption('sleep')) - self.nickused = int(getoption('nickused')) - self.maxchans = int(getoption('maxchans')) - self.maxnicks = int(getoption('maxnicks')) - self.usernotices = (getoption('usernotices') == 'on') - self.channotices = (getoption('channotices') == 'on') self.starttime = time.time() - self.file = None - if self.action == 'file': - self.filename = getoption('file') - if not self.filename: - print('Error: please specify file name with option "file=..."') - return - try: - self.file = open(self.filename, 'r') - except IOError: - print('Error: unable to open file "%s"' % self.filename) - return self.connect() if not self.quit: - if self.action == 'flood': - self.action_flood() - elif self.action == 'user': - self.action_user() - elif self.action == 'file': - self.action_file() + if self.args.file: + self.send_from_file() else: - print('Unknown action: "%s"' % self.action) - return + self.flood() + + def strrand(self, minlength=1, maxlength=50, spaces=False): + """Return string with random length and content.""" + length = random.randint(minlength, maxlength) + strspace = '' + if spaces: + strspace = ' ' + return ''.join(random.choice(string.ascii_uppercase + + string.ascii_lowercase + + string.digits + strspace) for x in range(length)) def send(self, data): """Send one message to client.""" - if getoption('debug') == 'on': + if self.args.debug: print('<-- %s' % data) msg = '%s\r\n' % data self.outbytes += len(msg) @@ -179,7 +107,7 @@ class Client: def recv(self, data): """Read one message from client.""" - if getoption('debug') == 'on': + if self.args.debug: print('--> %s' % data) if data.startswith('PING '): args = data[5:] @@ -207,18 +135,18 @@ class Client: data = data.decode('UTF-8') self.inbytes += len(data) data = self.lastbuf + data - while 1: + while True: pos = data.find('\r\n') if pos < 0: break self.recv(data[0:pos]) - data = data[pos+2:] + data = data[pos + 2:] self.lastbuf = data def connect(self): """Tell client that connection is ok.""" try: - count = self.nickused + count = self.args.nickused while self.nick == '': self.read(0.1) if self.nick and count > 0: @@ -239,54 +167,56 @@ class Client: return None rnick = self.nick while rnick == self.nick: - rnick = self.channels[channel][random.randint(0, len(self.channels[channel])-1)] + rnick = self.channels[channel][random.randint(0, len(self.channels[channel]) - 1)] return rnick - def action_flood(self): + def flood(self): """Yay, funny stuff here! Flood client!""" - wait = int(getoption('wait')) - if wait > 0: - print('Wait %d seconds' % wait) - time.sleep(wait) + if self.args.wait > 0: + print('Wait %f seconds' % self.args.wait) + time.sleep(self.args.wait) sys.stdout.write('Flooding client..') sys.stdout.flush() try: while not self.quit: - self.read(self.sleep) + self.read(self.args.sleep) # global actions action = random.randint(1, 2) if action == 1: # join - if len(self.channels) < self.maxchans: - channel = '#%s' % strrand(1, 25) + if len(self.channels) < self.args.maxchans: + channel = '#%s' % self.strrand(1, 25) if not channel in self.channels: self.send(':%s!%s JOIN :%s' % (self.nick, self.addr[0], channel)) self.send(':%s 353 %s = %s :@%s' % (NAME, self.nick, channel, self.nick)) self.send(':%s 366 %s %s :End of /NAMES list.' % (NAME, self.nick, channel)) self.channels[channel] = [self.nick] - elif action == 2 and self.usernotices: - # notice - self.send(':%s!%s@%s NOTICE %s :%s' % ( - strrand(1, 10), strrand(1, 10), strrand(1, 10), self.nick, strrand(1, 400, True))) + elif action == 2 and 'user' in self.args.notice: + # notice for user + self.send(':%s!%s@%s NOTICE %s :%s' % (self.strrand(1, 10), self.strrand(1, 10), + self.strrand(1, 10), self.nick, + self.strrand(1, 400, True))) # actions for each channel for channel in self.channels: action = random.randint(1, 50) if action >= 1 and action <= 10: # join - if len(self.channels[channel]) < self.maxnicks: + if len(self.channels[channel]) < self.args.maxnicks: self.nicknumber += 1 - newnick = '%s%d' % (strrand(1, 5), self.nicknumber) - self.send(':%s!%s@%s JOIN :%s' % (newnick, strrand(1, 10), strrand(1, 10), channel)) + newnick = '%s%d' % (self.strrand(1, 5), self.nicknumber) + self.send(':%s!%s@%s JOIN :%s' % (newnick, self.strrand(1, 10), + self.strrand(1, 10), channel)) self.channels[channel].append(newnick) elif action == 11: # part/quit if len(self.channels[channel]) > 0: rnick = self.chan_randnick(channel) if rnick: - command = 'QUIT :%s' % strrand(1, 30) + command = 'QUIT :%s' % self.strrand(1, 30) if random.randint(1, 2) == 1: command = 'PART %s' % channel - self.send(':%s!%s@%s %s' % (rnick, strrand(1, 10), strrand(1, 10), command)) + self.send(':%s!%s@%s %s' % (rnick, self.strrand(1, 10), + self.strrand(1, 10), command)) self.channels[channel].remove(rnick) elif action == 12: # kick @@ -294,29 +224,33 @@ class Client: rnick1 = self.chan_randnick(channel) rnick2 = self.chan_randnick(channel) if rnick1 and rnick2 and rnick1 != rnick2: - self.send(':%s!%s@%s KICK %s %s :%s' % (rnick1, strrand(1, 10), strrand(1, 10), channel, rnick2, strrand (1, 50))) + self.send(':%s!%s@%s KICK %s %s :%s' % (rnick1, self.strrand(1, 10), + self.strrand(1, 10), channel, rnick2, + self.strrand(1, 50))) self.channels[channel].remove(rnick2) else: # message if len(self.channels[channel]) > 0: rnick = self.chan_randnick(channel) if rnick: - msg = strrand(1, 400, True) - if self.channotices and random.randint(1,100) == 100: - # notice - self.send(':%s!%s@%s NOTICE %s :%s' % (rnick, strrand(1, 10), strrand (1, 10), channel, msg)) + msg = self.strrand(1, 400, True) + if 'channel' in self.args.notice and random.randint(1, 100) == 100: + # notice for channel + self.send(':%s!%s@%s NOTICE %s :%s' % (rnick, self.strrand(1, 10), + self.strrand(1, 10), channel, msg)) else: # add random highlight if random.randint(1, 100) == 100: msg = '%s: %s' % (self.nick, msg) - action2 = random.randint(1,50) + action2 = random.randint(1, 50) if action2 == 1: # action (/me) msg = '\x01ACTION %s\x01' % msg elif action2 == 2: # version msg = '\x01VERSION\x01' - self.send(':%s!%s@%s PRIVMSG %s :%s' % (rnick, strrand(1, 10), strrand(1, 10), channel, msg)) + self.send(':%s!%s@%s PRIVMSG %s :%s' % (rnick, self.strrand(1, 10), + self.strrand(1, 10), channel, msg)) # display progress if self.outcount % 1000 == 0: sys.stdout.write('.') @@ -335,37 +269,29 @@ class Client: self.endmsg = 'quit received' return - def action_user(self): - """User enters messages to send to client.""" - try: - while 1: - sys.stdout.write('Message to send to client: ') - sys.stdout.flush() - message = sys.stdin.readline() - self.send(message) - except Exception as e: - self.endmsg = 'connection lost' - self.endexcept = e - return - except KeyboardInterrupt: - self.endmsg = 'interrupted' - return - - def action_file(self): + def send_from_file(self): """Send messages from a file to client.""" + stdin = self.args.file == sys.stdin + count = 0 try: - count = 0 - for line in self.file: - if not line.startswith('//'): - self.read(self.sleep) - self.send(line.replace('${nick}', self.nick)) + while True: + if stdin: + sys.stdout.write('Message to send to client: ') + sys.stdout.flush() + message = self.args.file.readline() + if not message: + break + message = message.rstrip('\n') + if sys.version_info < (3,): + message = message.decode('UTF-8') + if not message.startswith('//'): + if not stdin: + # sleep, only if commands come from a file + self.read(self.args.sleep) + self.send(message.replace('${nick}', self.nick)) count += 1 - self.file.close() - sys.stdout.write('%d messages sent, press Enter to restart server' % count) - sys.stdout.flush() - sys.stdin.readline() except IOError as e: - self.endmsg = 'unable to open file "%s"' % self.filename + self.endmsg = 'unable to read file %s' % self.args.file self.endexcept = e return except Exception as e: @@ -375,6 +301,15 @@ class Client: except KeyboardInterrupt: self.endmsg = 'interrupted' return + finally: + sys.stdout.write('\n') + sys.stdout.write('%d messages sent from %s, press Enter to exit' + % (count, 'stdin' if stdin else 'file')) + sys.stdout.flush() + try: + sys.stdin.readline() + except: + pass def stats(self): msgexcept = '' @@ -393,30 +328,54 @@ class Client: print('Closing connection with %s' % str(self.addr)) self.sock.close() -def main(): - if len(sys.argv) > 1 and (sys.argv[1] == '-h' or sys.argv[1] == '--help'): - usage() - readconfig('%s.conf' % NAME) - for arg in sys.argv: - setoption(arg) - print('%s %s - WeeChat IRC server' % (NAME, VERSION)) - while 1: - print('Options: %s' % getdictoptions()) - print('Listening on port %s (ctrl-C to exit)' % getoption('port')) - servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - servsock.bind((getoption('host'), int(getoption('port')))) - servsock.listen(1) - clientsock = None - addr = None - try: - clientsock, addr = servsock.accept() - except KeyboardInterrupt: - servsock.close() - return - print('Connection from %s' % str(addr)) - client = Client(clientsock, addr) - del client +# parse command line arguments +parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, + fromfile_prefix_chars='@', + description=DESCRIPTION, + epilog='Note: the environment variable "WEERCD_OPTIONS" can be ' + 'set with some default options, and argument "@file.txt" can be ' + 'used to read some default options in a file.') +parser.add_argument('-H', '--host', help='host for socket bind') +parser.add_argument('-p', '--port', type=int, default=7777, help='port for socket bind') +parser.add_argument('-f', '--file', type=argparse.FileType('r'), + help='send messages from file, instead of flooding the client (use "-" for stdin)') +parser.add_argument('-c', '--maxchans', type=int, default=5, help='max number of channels to join') +parser.add_argument('-n', '--maxnicks', type=int, default=100, help='max number of nicks per channel') +parser.add_argument('-u', '--nickused', type=int, default=0, + help='send 433 (nickname already in use) this number of times before accepting nick') +parser.add_argument('-N', '--notice', metavar='NOTICE_TYPE', choices=['user', 'channel'], + default=['user', 'channel'], nargs='*', + help='notices to send: "user" (to user), "channel" (to channel)') +parser.add_argument('-s', '--sleep', type=float, default=0, + help='sleep for select: delay between 2 messages sent to client (float, in seconds)') +parser.add_argument('-w', '--wait', type=float, default=0, + help='time to wait before flooding client (float, in seconds)') +parser.add_argument('-d', '--debug', action='store_true', help='debug output') +parser.add_argument('-v', '--version', action='version', version=VERSION) +args = parser.parse_args(shlex.split(os.getenv('WEERCD_OPTIONS') or '') + sys.argv[1:]) -if __name__ == '__main__': - main() +print('%s %s - WeeChat IRC testing server' % (NAME, VERSION)) +print('Options: %s' % vars(args)) + +while True: + servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + servsock.bind((args.host or '', args.port)) + servsock.listen(1) + except Exception as e: + print('Socket error: %s' % e) + sys.exit(1) + print('Listening on port %s (ctrl-C to exit)' % args.port) + clientsock = None + addr = None + try: + clientsock, addr = servsock.accept() + except KeyboardInterrupt: + servsock.close() + sys.exit(0) + print('Connection from %s' % str(addr)) + client = Client(clientsock, addr, args) + del client + if args.file: + break