Created
August 5, 2011 18:34
-
-
Save thomasleveil/1128184 to your computer and use it in GitHub Desktop.
hopefully solves issue described at http://forum.bigbrotherbot.net/general-usage-support/problem-with-commands/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# BigBrotherBot(B3) (www.bigbrotherbot.net) | |
# Copyright (C) 2005 Michael "ThorN" Thornton | |
# | |
# 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 | |
# | |
# Changelog : | |
# 2009/12/06 - 1.3.5 - Courgette | |
# * make default socket_timeout 800 ms | |
# * custom socket_timeout and maxRetries can be specified on a per | |
# call basis | |
# 2009/12/11 - 1.3.6 - Courgette | |
# * make errors warnings until maxRetries is not reached | |
# 2011/2/1 - 1.3.7 - Bravo17 | |
# * added variables for rcon & qserver send & reply strings | |
# 2011/04/02 - 1.3.8 - Just a baka | |
# * Quit command should never be retried | |
# 2011/04/13 - 1.3.9 - Courgette | |
# * hopefully filter out non ascii characters | |
# 2011/04/13 - 1.3.10 - Courgette | |
# * should get rid of UnicodeDecodeError | |
# 2011/04/20 - 1.4 - Courgette | |
# * now sent data is encoded as UTF-8 | |
# 2011/08/05 - 1.5 - Courgette | |
# * reverts back as it was in 1.3.8 after report by -real- at http://forum.bigbrotherbot.net/general-usage-support/problem-with-commands/ | |
# | |
__author__ = 'ThorN' | |
__version__ = '1.5.0' | |
import socket | |
import sys | |
import select | |
import re | |
import time | |
import thread | |
import threading | |
import Queue | |
from b3.lib.beaker.cache import CacheManager | |
from b3.lib.beaker.util import parse_cache_config_options | |
#-------------------------------------------------------------------------------------------------- | |
class Rcon: | |
host = () | |
password = None | |
lock = thread.allocate_lock() | |
socket = None | |
queue = None | |
console = None | |
socket_timeout = 0.80 | |
rconsendstring = '\377\377\377\377rcon "%s" %s\n' | |
rconreplystring = '\377\377\377\377print\n' | |
qserversendstring = '\377\377\377\377%s\n' | |
#caching options | |
cache_opts = { | |
'cache.data_dir': 'b3/cache/data', | |
'cache.lock_dir': 'b3/cache/lock', | |
} | |
#create cache | |
cache = CacheManager(**parse_cache_config_options(cache_opts)) | |
#default expiretime for the status cache in seconds and cache type | |
statusCacheExpireTime = 2 | |
statusCacheType = 'memory' | |
def __init__(self, console, host, password): | |
self.console = console | |
self.queue = Queue.Queue() | |
if self.console.config.has_option('caching', 'status_cache_type'): | |
self.statusCacheType = self.console.config.get('caching', 'status_cache_type').lower() | |
if self.statusCacheType not in ['file', 'memory']: | |
self.statusCacheType = 'memory' | |
if self.console.config.has_option('caching', 'status_cache_expire'): | |
self.statusCacheExpireTime = abs(self.console.config.getint('caching', 'status_cache_expire')) | |
if self.statusCacheExpireTime > 5: | |
self.statusCacheExpireTime = 5 | |
self.console.bot('rcon status Cache Expire Time: [%s sec] Type: [%s]' %(self.statusCacheExpireTime, self.statusCacheType)) | |
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
self.host = host | |
self.password = password | |
self.socket.settimeout(2) | |
self.socket.connect(self.host) | |
self._stopEvent = threading.Event() | |
thread.start_new_thread(self._writelines, ()) | |
def send(self, data, maxRetries=None, socketTimeout=None): | |
if socketTimeout is None: | |
socketTimeout = self.socket_timeout | |
if maxRetries is None: | |
maxRetries = 2 | |
data = data.strip() | |
self.console.verbose('QSERVER sending (%s:%s) %r', self.host[0], self.host[1], data) | |
startTime = time.time() | |
retries = 0 | |
while time.time() - startTime < 5: | |
readables, writeables, errors = select.select([], [self.socket], [self.socket], socketTimeout) | |
if len(errors) > 0: | |
self.console.warning('QSERVER: %r', errors) | |
elif len(writeables) > 0: | |
try: | |
writeables[0].send(self.qserversendstring % data) | |
except Exception, msg: | |
self.console.warning('QSERVER: ERROR sending: %r', msg) | |
else: | |
try: | |
data = self.readSocket(self.socket, socketTimeout=socketTimeout) | |
self.console.verbose2('QSERVER: Received %r' % data) | |
return data | |
except Exception, msg: | |
self.console.warning('QSERVER: ERROR reading: %r', msg) | |
else: | |
self.console.verbose('QSERVER: no writeable socket') | |
time.sleep(0.05) | |
retries += 1 | |
if retries >= maxRetries: | |
self.console.error('QSERVER: too much tries. Abording (%r)', data.strip()) | |
break | |
self.console.verbose('QSERVER: retry sending %r (%s/%s)...', data.strip(), retries, maxRetries) | |
self.console.debug('QSERVER: Did not send any data') | |
return '' | |
def sendRcon(self, data, maxRetries=None, socketTimeout=None): | |
if socketTimeout is None: | |
socketTimeout = self.socket_timeout | |
if maxRetries is None: | |
maxRetries = 2 | |
data = data.strip() | |
self.console.verbose('RCON sending (%s:%s) %r', self.host[0], self.host[1], data) | |
startTime = time.time() | |
retries = 0 | |
while time.time() - startTime < 5: | |
readables, writeables, errors = select.select([], [self.socket], [self.socket], socketTimeout) | |
if len(errors) > 0: | |
self.console.warning('RCON: %r', errors) | |
elif len(writeables) > 0: | |
try: | |
writeables[0].send(self.rconsendstring % (self.password, data)) | |
except Exception, msg: | |
self.console.warning('RCON: ERROR sending: %r', msg) | |
else: | |
try: | |
data = self.readSocket(self.socket, socketTimeout=socketTimeout) | |
self.console.verbose2('RCON: Received %r' % data) | |
return data | |
except Exception, msg: | |
self.console.warning('RCON: ERROR reading: %r', msg) | |
if re.match(r'^quit|map(_rotate)?.*', data): | |
# do not retry quits and map changes since they prevent the server from responding | |
self.console.verbose2('RCON: no retry for %r', data) | |
return '' | |
else: | |
self.console.verbose('RCON: no writeable socket') | |
time.sleep(0.05) | |
retries += 1 | |
if retries >= maxRetries: | |
self.console.error('RCON: too much tries. Abording (%r)', data.strip()) | |
break | |
self.console.verbose('RCON: retry sending %r (%s/%s)...', data.strip(), retries, maxRetries) | |
self.console.debug('RCON: Did not send any data') | |
return '' | |
def stop(self): | |
"""Stop the rcon writelines queue""" | |
self._stopEvent.set() | |
def _writelines(self): | |
while not self._stopEvent.isSet(): | |
lines = self.queue.get(True) | |
self.lock.acquire() | |
try: | |
data = '' | |
i = 0 | |
for cmd in lines: | |
if i > 0: | |
# pause and give time for last send to finish | |
time.sleep(1) | |
if not cmd: | |
continue | |
d = self.sendRcon(cmd) | |
if d: | |
data += d | |
i+=1 | |
finally: | |
self.lock.release() | |
def writelines(self, lines): | |
self.queue.put(lines) | |
def write(self, cmd, maxRetries=None, socketTimeout=None, Cached=True): | |
#intercept status request for caching construct | |
if cmd == 'status' and Cached: | |
status_cache = self.cache.get_cache('status', type=self.statusCacheType, expire=self.statusCacheExpireTime) | |
data = status_cache.get(key='status', createfunc=self._requestStatusCached) | |
return data | |
self.lock.acquire() | |
try: | |
data = self.sendRcon(cmd, maxRetries=maxRetries, socketTimeout=socketTimeout) | |
finally: | |
self.lock.release() | |
if data: | |
return data | |
else: | |
return '' | |
def _requestStatusCached(self): | |
self.lock.acquire() | |
try: | |
data = self.sendRcon('status', maxRetries=5) | |
finally: | |
self.lock.release() | |
if data: | |
return data | |
else: | |
return '' | |
def flush(self): | |
pass | |
def readNonBlocking(self, sock): | |
sock.settimeout(2) | |
startTime = time.time() | |
data = '' | |
while time.time() - startTime < 1: | |
try: | |
d = str(sock.recv(4096)) | |
except socket.error, detail: | |
self.console.debug('RCON: ERROR reading: %s' % detail) | |
break | |
else: | |
if d: | |
# remove rcon header | |
data += d.replace(self.rconreplystring, '') | |
elif len(data) > 0 and ord(data[-1:]) == 10: | |
break | |
return data.strip() | |
def readSocket(self, sock, size=4096, socketTimeout=None): | |
if socketTimeout is None: | |
socketTimeout = self.socket_timeout | |
data = '' | |
readables, writeables, errors = select.select([sock], [], [sock], socketTimeout) | |
if not len(readables): | |
raise Exception('No readable socket') | |
while len(readables): | |
d = str(sock.recv(size)) | |
if d: | |
# remove rcon header | |
data += d.replace(self.rconreplystring, '') | |
readables, writeables, errors = select.select([sock], [], [sock], socketTimeout) | |
if len(readables): | |
self.console.verbose('RCON: More data to read in socket') | |
return data.strip() | |
def close(self): | |
pass | |
def getRules(self): | |
self.lock.acquire() | |
try: | |
data = self.send('getstatus') | |
finally: | |
self.lock.release() | |
if data: | |
return data | |
else: | |
return '' | |
def getInfo(self): | |
self.lock.acquire() | |
try: | |
data = self.send('getinfo') | |
finally: | |
self.lock.release() | |
if data: | |
return data | |
else: | |
return '' | |
if __name__ == '__main__': | |
""" | |
To run tests : python b3/parsers/q3a_rcon.py <rcon_ip> <rcon_port> <rcon_password> | |
""" | |
from b3.fake import fakeConsole | |
r = Rcon(fakeConsole, (sys.argv[1], int(sys.argv[2])), sys.argv[3]) | |
for cmd in ['say "test1"', 'say "test2"', 'say "test3"', 'say "test4"', 'say "test5"']: | |
fakeConsole.info('Writing %s', cmd) | |
data = r.write(cmd) | |
fakeConsole.info('Recieved %s', data) | |
print '----------------------------------------' | |
for cmd in ['say "test1"', 'say "test2"', 'say "test3"', 'say "test4"', 'say "test5"']: | |
fakeConsole.info('Writing %s', cmd) | |
data = r.write(cmd, socketTimeout=0.45) | |
fakeConsole.info('Recieved %s', data) | |
print '----------------------------------------' | |
for cmd in ['.B3', '.Administrator', '.Admin', 'status', 'sv_mapRotation', 'players']: | |
fakeConsole.info('Writing %s', cmd) | |
data = r.write(cmd) | |
fakeConsole.info('Recieved %s', data) | |
print '----------------------------------------' | |
for cmd in ['.B3', '.Administrator', '.Admin', 'status', 'sv_mapRotation', 'players']: | |
fakeConsole.info('Writing %s', cmd) | |
data = r.write(cmd, socketTimeout=0.55) | |
fakeConsole.info('Recieved %s', data) | |
print '----------------------------------------' | |
fakeConsole.info('getRules') | |
data = r.getRules() | |
fakeConsole.info('Recieved %s', data) | |
print '----------------------------------------' | |
fakeConsole.info('getInfo') | |
data = r.getInfo() | |
fakeConsole.info('Recieved %s', data) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment