#!/usr/bin/env python # -*- coding: utf-8 -*- """ Arena Lurker ============ A `Quake III Arena`_ server scanner and status icon notifier. .. note:: Quitting via popup menu is flawed and needs to be fixed. TODO ---- - Scan full 27960-27963 port range. :Copyright: 2007-2008 Jochen Kupperschmidt :Date: 06-May-2008 :License: GNU General Public License, version 2 :Version: 0.1 .. _Quake III Arena: http://www.idsoftware.com/games/quake/quake3-arena/ """ from __future__ import with_statement from contextlib import contextmanager import socket import threading from time import sleep import gtk import pygtk # network/parsing stuff def process_payload(data, addr): type_, content = data[4:].split('\n', 1) print '%s received from %s:%s' % (type_, addr[0], addr[1]) if type_ == 'infoResponse': info = process_info_response(content) print '\t%(mapname)s (%(clients)s/%(sv_maxclients)s) "%(hostname)s"' \ % info return info def process_info_response(data): """Parse `\foo\1\bar\2\baz\3` into `{'foo': 1, 'bar': 2, 'baz': 3}`.""" items = data.split('\\')[1:] return dict(zip(items[::2], items[1::2])) class Scanner(object): """A socket wrapper. Implements the context manager protocol (:PEP:`343`). """ def __init__(self, timeout=4): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.settimeout(timeout) self.socket.bind(('', 0)) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.socket.close() def scan(self, port=27960): """Send broadcast query and collect responses.""" request = '\xFF\xFF\xFF\xFFgetinfo xxx' self.socket.sendto(request, ('<broadcast>', port)) while True: try: data, addr = self.socket.recvfrom(1024) yield process_payload(data, addr) except socket.timeout: break # GTK stuff @contextmanager def acquire_gdk_lock(): """Provide a context for use with the `with` statement.""" try: gtk.gdk.threads_enter() yield finally: gtk.gdk.threads_leave() def quit(widget=None, data=None): if data: data.set_visible(False) gtk.main_quit() class Icon(gtk.StatusIcon): def __init__(self): gtk.StatusIcon.__init__(self) self.set_from_stock(gtk.STOCK_EXECUTE) # Add a popup menu with quit option. menu = gtk.Menu() menu_item = gtk.ImageMenuItem(gtk.STOCK_QUIT) menu_item.connect('activate', quit, self) menu.append(menu_item) self.connect('popup-menu', self.popup_menu, menu) self.set_visible(True) def update(self, servers): """Update icon and tooltip according to servers found.""" name = 'YES' if servers else 'NO' with acquire_gdk_lock(): stock_id = getattr(gtk, 'STOCK_' + name) self.set_from_stock(stock_id) self.set_tooltip('%d server(s)' % len(servers)) def popup_menu(self, widget, button, time, data=None): """Show popup menu when right mouse button is pressed (on the icon).""" if button == 3: if data: data.show_all() data.popup(None, None, None, 3, time) # application entry point class MainLoop(object): def __init__(self, scanner): self.stop = False self.scanner = scanner def join(self): self.stop = True def run(self): icon = Icon() gtk.gdk.threads_init() threading.Thread(target=gtk.main).start() while not self.stop: try: print '* Scanning ...' servers = list(self.scanner.scan()) icon.update(servers) sleep(5) except KeyboardInterrupt: print 'Ctrl-C pressed, aborting.' break quit() if __name__ == '__main__': with Scanner() as scanner: MainLoop(scanner).run()