The Python listening script is now in production. With a few luser-type problems along the way. Specifically: I have discovered, quite recently, that having the same name for both the NOTIFY command and the LISTEN command is kind of a good strategy when you actually want your listener to be notified. The problem? The code was listening for notify_port_updated. The code was generating a port_updated. Remember how I was talking about Data driven listening? Well, perhaps I should have spent more time reading the data instead of getting the code to be driven by it.
That said, the solution is in and working. The final code is here:
#!/usr/bin/env python # # $Id: fp-listen.py,v 1.2 2006/09/11 01:49:16 dan Exp $ # # This program listens for events on the database and processes them # # # configuration items # DSN = 'dbname=freshports.org user=dan' CACHEPATH = '/usr/websites/beta.freshports.org/dynamic/caching/cache/ports/%s/%s.Detail.html' import sys, psycopg, select import os # for deleting cache files import syslog # for logging def RemoveCacheEntry(): syslog.syslog(syslog.LOG_NOTICE, 'checking for cache entries to remove...') dbh = psycopg.connect(DSN) dbh.autocommit(0) curs = dbh.cursor() curs.execute("SELECT id, port_id, category, port FROM cache_clearing_ports ORDER BY id") NumRows = curs.rowcount dbh.commit(); if (NumRows > 0): syslog.syslog(syslog.LOG_NOTICE, 'COUNT: %d entries to process' % (NumRows)) for row in curs.dictfetchall(): filename = CACHEPATH % (row['category'], row['port']) syslog.syslog(syslog.LOG_NOTICE, 'removing %s' % (filename)) try: os.remove(filename) except OSError, err: if err[0] == 2: pass # no file to delete, so no worries else: syslog.syslog(syslog.LOG_CRIT, 'ERROR: error deleting cache entry. Error message is %s' % (err)) continue # end if syslog.syslog(syslog.LOG_NOTICE, "DELETE FROM cache_clearing_ports WHERE id = %d" % (row['id'])) curs.execute("DELETE FROM cache_clearing_ports WHERE id = %d" % (row['id'])) dbh.commit() # end for else: syslog.syslog(syslog.LOG_ERR, 'ERROR: No cached entries found for removal') # end if syslog.syslog(syslog.LOG_NOTICE, 'finished') return NumRows def ProcessPortsMoved(): syslog.syslog(syslog.LOG_NOTICE, 'processing ports/MOVED') def ProcessPortsUpdating(): syslog.syslog(syslog.LOG_NOTICE, 'processing ports/UPDATING') def ProcessVUXML(): syslog.syslog(syslog.LOG_NOTICE, 'processing ports/security/portaudit/vuln.xml') syslog.openlog('fp-listen') syslog.syslog(syslog.LOG_NOTICE, 'Starting up') print "Opening connection using dns:", DSN conn = psycopg.connect(DSN) conn.autocommit(1) curs = conn.cursor() curs.execute("SELECT name, script_name FROM listen_for ORDER BY id") listens_for = curs.fetchall() listens = dict() for listen in listens_for: curs.execute("LISTEN %s" % listen[0]) listens[listen[0]] = listen[1] print listen print "Now listening..." while 1: select.select([curs],[],[])==([],[],[]) curs.execute("SELECT 1") syslog.syslog(syslog.LOG_NOTICE, 'Just woke up! *************') notifies = curs.notifies() for n in notifies: print "got %s" % n[0] print "this is index %s" % listens[n[0]] # in real life, do something with each... print "got %s and I need to call %s" % (n[0], listens[n[0]]) syslog.syslog(syslog.LOG_NOTICE, "got %s and I need to call %s" % (n[0], listens[n[0]])) if listens.has_key(n[0]): if listens[n[0]] == 'listen_port': RemoveCacheEntry() elif listens[n[0]] == 'listen_ports_moved': ProcessPortsMoved() elif listens[n[0]] == 'listen_ports_updating': ProcessPortsUpdating() elif listens[n[0]] == 'listen_vuxml': ProcessVUXML() else: syslog.syslog(syslog.LOG_ERR, "Code does not know what to do when '%s' is found." % n[0]) else: syslog.syslog(syslog.LOG_NOTICE, 'no such key!') logging.error('terminating')
All that fluff about ports/MOVED, ports/UPDATING, etc… it will eventually be implemented. I’m planning on changing strategy though. At the moment, such files are processed in between commits; the FP daemon stops processing, and runs through the ports/MOVED file before proceeding. That will change.
And the trigger is:
CREATE OR REPLACE FUNCTION ports_clear_cache() RETURNS TRIGGER AS $$ DECLARE l_cache_clearing_ports_id int8; l_port text; l_category text; BEGIN IF TG_OP = 'UPDATE' THEN SELECT id INTO l_cache_clearing_ports_id FROM cache_clearing_ports WHERE port_id = NEW.id; IF NOT FOUND THEN SELECT category, name INTO l_category, l_port FROM ports_all WHERE id = NEW.id; INSERT INTO cache_clearing_ports (port_id, category, port) VALUES (NEW.id, l_category, l_port); END IF; NOTIFY "port_updated"; END IF; -- when a port changes, add an entry to the cache clearing table RETURN NEW; END $$ LANGUAGE 'plpgsql'; DROP TRIGGER ports_clear_cache ON ports; CREATE TRIGGER ports_clear_cache AFTER UPDATE on ports FOR EACH ROW EXECUTE PROCEDURE ports_clear_cache();