[TPG] Misc changes for new dispense
[zanchey/dispense2.git] / sql-edition / servers / VendServer.py
index 09bfe66..e18d8a3 100755 (executable)
@@ -1,20 +1,67 @@
 #!/usr/bin/python
 # vim:ts=4
 
-import sys, os, string, re, pwd
-import pg
-from time import time, sleep
+USE_DB = 0
+USE_MIFARE = 1
+
+import ConfigParser
+import sys, os, string, re, pwd, signal, math, syslog
+import logging, logging.handlers
+from traceback import format_tb
+if USE_DB: import pg
+from time import time, sleep, mktime, localtime
 from popen2 import popen2
-from LATClient import LATClient
-from VendingMachine import VendingMachine
-from ConfigParser import ConfigParser
+from LATClient import LATClient, LATClientException
+from SerialClient import SerialClient, SerialClientException
+from VendingMachine import VendingMachine, VendingException
+from MessageKeeper import MessageKeeper
+from HorizScroll import HorizScroll
+from random import random, seed
+from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
+from SnackConfig import get_snack#, get_snacks
+import socket
+from posix import geteuid
+from LDAPConnector import get_uid, set_card_id
+
+CREDITS="""
+This vending machine software brought to you by:
+Bernard Blackham
+Mark Tearle
+Nick Bannon
+Cameron Patrick
+and a collective of hungry alpacas.
+
+
+
+For a good time call +61 8 6488 3901
+
+
+
+"""
 
-GREETING = 'UCC SNACKS'
 PIN_LENGTH = 4
 
 DOOR = 1
 SWITCH = 2
 KEY = 3
+TICK = 4
+MIFARE = 5
+
+
+(
+STATE_IDLE,
+STATE_DOOR_OPENING,
+STATE_DOOR_CLOSING,
+STATE_GETTING_UID,
+STATE_GETTING_PIN,
+STATE_GET_SELECTION,
+STATE_GRANDFATHER_CLOCK,
+) = range(1,8)
+
+TEXT_SPEED = 0.8
+IDLE_SPEED = 0.05
+
+class DispenseDatabaseException(Exception): pass
 
 class DispenseDatabase:
        def __init__(self, vending_machine, host, name, user, password):
@@ -23,16 +70,15 @@ class DispenseDatabase:
                self.db.query('LISTEN vend_requests')
 
        def process_requests(self):
-               print 'processing'
+               logging.debug('database processing')
                query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
                try:
                        outstanding = self.db.query(query).getresult()
                except (pg.error,), db_err:
-                       sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
-                       return
+                       raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
                for (id, slot) in outstanding:
                        (worked, code, string) = self.vending_machine.vend(slot)
-                       print (worked, code, string)
+                       logging.debug (str((worked, code, string)))
                        if worked:
                                query = 'SELECT vend_success(%s)'%id
                                self.db.query(query).getresult()
@@ -46,240 +92,1036 @@ class DispenseDatabase:
                        self.process_requests()
                        notify = self.db.getnotify()
 
+def scroll_options(username, mk, welcome = False):
+       if welcome:
+               # Balance checking: crap code, [DAA]'s fault
+               # Updated 2011 to handle new dispense [MRD]
+               raw_acct = os.popen('dispense acct %s' % username)
+               acct = raw_acct.read()
+               # this is fucking appalling
+               balance = acct[acct.find("$")+1:acct.find("(")].strip()
+               raw_acct.close()
+        
+               msg = [(center('WELCOME'), False, TEXT_SPEED),
+                          (center(username), False, TEXT_SPEED),
+                          (center(balance), False, TEXT_SPEED),]
+       else:
+               msg = []
+       choices = ' '*10+'CHOICES: '
+
+       # Get coke contents
+       cokes = []
+       for i in range(0, 7):
+               cmd = 'dispense iteminfo coke:%i' % i
+               raw = os.popen(cmd)
+               info = raw.read()
+               raw.close()
+               m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
+               cents = int(m.group(1))*100 + int(m.group(2))
+               cokes.append('%i %i %s' % (i, cents, m.group(3)));
+
+       for c in cokes:
+               c = c.strip()
+               (slot_num, price, slot_name) = c.split(' ', 2)
+               if slot_name == 'dead': continue
+               choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
+
+#      we don't want to print snacks for now since it'll be too large
+#      and there's physical bits of paper in the machine anyway - matt
+#      try:
+#              snacks = get_snacks()
+#      except:
+#              snacks = {}
+#
+#      for slot, ( name, price ) in snacks.items():
+#              choices += '%s8-%s (%sc) ' % ( slot, name, price )
+
+       choices += '55-DOOR '
+       choices += 'OR ANOTHER SNACK. '
+       choices += '99 TO READ AGAIN. '
+       choices += 'CHOICE?   '
+       msg.append((choices, False, None))
+       mk.set_messages(msg)
+
 def get_pin(uid):
        try:
                info = pwd.getpwuid(uid)
        except KeyError:
+               logging.info('getting pin for uid %d: user not in password file'%uid)
                return None
        if info.pw_dir == None: return False
        pinfile = os.path.join(info.pw_dir, '.pin')
        try:
                s = os.stat(pinfile)
        except OSError:
+               logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
                return None
        if s.st_mode & 077:
-               return None
+               logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
+               os.chmod(pinfile, 0600)
        try:
                f = file(pinfile)
        except IOError:
+               logging.info('getting pin for uid %d: I cannot read pin file'%uid)
                return None
        pinstr = f.readline()
        f.close()
        if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
+               logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
                return None
        return int(pinstr)
 
 def has_good_pin(uid):
        return get_pin(uid) != None
 
-def verify_user_pin(uid, pin):
-       if get_pin(uid) == pin:
+def verify_user_pin(uid, pin, skip_pin_check=False):
+       if skip_pin_check or get_pin(uid) == pin:
                info = pwd.getpwuid(uid)
+               if skip_pin_check:
+                       logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
+               else:
+                       logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
                return info.pw_name
        else:
+               logging.info('refused pin for uid %d'%(uid))
                return None
 
-def door_open_mode(vending_machine):
-       print "Entering open door mode"
-       v.display("DOOR  OPEN")
-       while True:
-               e = v.next_event()
-               if e == None: break
-               (event, params) = e
-               if event == DOOR:
-                       if params == 1: # door closed
-                               v.display("BYE BYE!")
-                               sleep(1)
-                               return
+
+def cookie(v):
+       seed(time())
+       messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
+       choice = int(random()*len(messages))
+       msg = messages[choice]
+       left = range(len(msg))
+       for i in range(len(msg)):
+               if msg[i] == ' ': left.remove(i)
+       reveal = 1
+       while left:
+               s = ''
+               for i in range(0, len(msg)):
+                       if i in left:
+                               if reveal == 0:
+                                       left.remove(i)
+                                       s += msg[i]
+                               else:
+                                       s += chr(int(random()*26)+ord('A'))
+                               reveal += 1
+                               reveal %= 17
+                       else:
+                               s += msg[i]
+               v.display(s)
 
 def center(str):
        LEN = 10
        return ' '*((LEN-len(str))/2)+str
 
-class MessageKeeper:
-       def __init__(self, vendie):
-               self.scrolling_message = []
-               self.v = vendie
-               self.next_update = None
 
-       def set_message(self, string):
-               self.scrolling_message = [(string, False, None)]
-               self.update_display(True)
 
-       def set_messages(self, strings):
-               self.scrolling_message = strings
-               self.update_display(True)
+idlers = []
+idler = None
+
+def setup_idlers(v):
+       global idlers, idler
+       idlers = [
+                GrayIdler(v),
+               StringIdler(v, text="Kill 'em all", repeat=False),
+                GrayIdler(v,one="*",zero="-"),
+               StringIdler(v, text=CREDITS),
+                GrayIdler(v,one="/",zero="\\"),
+               ClockIdler(v),
+                GrayIdler(v,one="X",zero="O"),
+               FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
+                GrayIdler(v,one="*",zero="-",reorder=1),
+               StringIdler(v, text=str(math.pi) + "            "),
+               ClockIdler(v),
+                GrayIdler(v,one="/",zero="\\",reorder=1),
+               StringIdler(v, text=str(math.e) + "            "),
+                GrayIdler(v,one="X",zero="O",reorder=1),
+               StringIdler(v, text="    I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
+               PipeIdler(v, "/usr/bin/getent", "passwd"),
+               FortuneIdler(v),
+               ClockIdler(v),
+               StringIdler(v),
+               TrainIdler(v),
+               ]
+       disabled = [
+               ]
+
+def reset_idler(v, vstatus, t = None):
+       global idlers, idler
+       idler = GreetingIdler(v, t)
+       vstatus.time_of_next_idlestep = time()+idler.next()
+       vstatus.time_of_next_idler = None
+       vstatus.time_to_autologout = None
+       vstatus.change_state(STATE_IDLE, 1)
+
+def choose_idler():
+       global idlers, idler
+       iiindex = 0
+       average_affinity = 10 # guessing here...
+
+       if idler and idler.__class__ != GreetingIdler:
+               iiindex = idlers.index(idler)
+
+       iilen = len(idlers)
+
+       move = int(random()*len(idlers)*average_affinity) + 1
+
+       while move >= 0:
+               iiindex += 1
+               iiindex %= iilen
+               idler = idlers[iiindex]
+               move -= idler.affinity()
+
+       idler.reset()
+
+def idle_step(vstatus):
+       global idler
+       if idler.finished():
+               choose_idler()
+               vstatus.time_of_next_idler = time() + 30
+       nextidle = idler.next()
+       if nextidle is None:
+               nextidle = IDLE_SPEED
+       vstatus.time_of_next_idlestep = time()+nextidle
+
+class VendState:
+       def __init__(self,v):
+               self.state_table = {}
+               self.state = STATE_IDLE
+               self.counter = 0
+
+               self.mk = MessageKeeper(v)
+               self.cur_user = ''
+               self.cur_pin = ''
+               self.username = ''
+               self.cur_selection = ''
+               self.time_to_autologout = None
+
+               self.last_timeout_refresh = None
+
+       def change_state(self,newstate,newcounter=None):
+               if self.state != newstate:
+                       #print "Changing state from: ", 
+                       #print self.state,
+                       #print " to ", 
+                       #print newstate 
+                       self.state = newstate
+
+               if newcounter is not None and self.counter != newcounter:
+                       #print "Changing counter from: ", 
+                       #print self.counter,
+                       #print " to ", 
+                       #print newcounter 
+                       self.counter = newcounter
+
+
+
+def handle_tick_event(event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_switch_event(event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+
+def do_nothing(state, event, params, v, vstatus):
+       print "doing nothing (s,e,p)", state, " ", event, " ", params
+       pass
+
+def handle_getting_uid_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_getting_pin_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_get_selection_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       ###
+       ### State logging out ..
+       if vstatus.time_to_autologout != None:
+               time_left = vstatus.time_to_autologout - time()
+               if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
+                       vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
+                       vstatus.last_timeout_refresh = int(time_left)
+                       vstatus.cur_selection = ''
+
+       if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
+               vstatus.time_to_autologout = None
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+               vstatus.cur_selection = ''
+                       
+               reset_idler(v, vstatus)
+
+       ### State fully logged out ... reset variables
+       if vstatus.time_to_autologout and not vstatus.mk.done(): 
+               vstatus.time_to_autologout = None
+       if vstatus.cur_user == '' and vstatus.time_to_autologout: 
+               vstatus.time_to_autologout = None
+       
+       ### State logged in
+       if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
+               # start autologout
+               vstatus.time_to_autologout = time() + 15
+               vstatus.last_timeout_refresh = None
 
-       def update_display(self, forced = False):
-               if not forced and self.next_update != None and time() < self.next_update:
+       ## FIXME - this may need to be elsewhere.....
+       # need to check
+       vstatus.mk.update_display()
+
+
+
+def handle_get_selection_key(state, event, params, v, vstatus):
+       key = params
+       if len(vstatus.cur_selection) == 0:
+               if key == 11:
+                       vstatus.cur_pin = ''
+                       vstatus.cur_user = ''
+                       vstatus.cur_selection = ''
+                       
+                       vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
+                       reset_idler(v, vstatus, 2)
+                       return
+               vstatus.cur_selection += chr(key + ord('0'))
+               vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
+               vstatus.time_to_autologout = None
+       elif len(vstatus.cur_selection) == 1:
+               if key == 11:
+                       vstatus.cur_selection = ''
+                       vstatus.time_to_autologout = None
+                       scroll_options(vstatus.username, vstatus.mk)
                        return
-               if len(self.scrolling_message) > 0:
-                       newmsg = self.scrolling_message[0]
-                       if newmsg[2] != None:
-                               self.next_update = time() + newmsg[2]
+               else:
+                       vstatus.cur_selection += chr(key + ord('0'))
+                       if vstatus.cur_user:
+                               make_selection(v,vstatus)
+                               vstatus.cur_selection = ''
+                               vstatus.time_to_autologout = time() + 8
+                               vstatus.last_timeout_refresh = None
                        else:
-                               self.next_update = None
-                       self.v.display(self.scrolling_message[0][0])
-                       if self.scrolling_message[0][1]:
-                               self.scrolling_message.append(self.scrolling_message[0])
-                       del self.scrolling_message[0]
+                               # Price check mode.
+                               price_check(v,vstatus)
+                               vstatus.cur_selection = ''
+                               vstatus.time_to_autologout = None
+                               vstatus.last_timeout_refresh = None
 
-if __name__ == '__main__':
-       cp = ConfigParser()
-       cp.read('/etc/dispense/servers.conf')
-       DBServer = cp.get('Database', 'Server')
-       DBName = cp.get('Database', 'Name')
-       DBUser = cp.get('VendingMachine', 'DBUser')
-       DBPassword = cp.get('VendingMachine', 'DBPassword')
-
-       ServiceName = cp.get('VendingMachine', 'ServiceName')
-       ServicePassword = cp.get('VendingMachine', 'Password')
-       # Open vending machine via LAT
-       if 0:
-               latclient = LATClient(service = ServiceName, password = ServicePassword)
-               (rfh, wfh) = latclient.get_fh()
+def make_selection(v, vstatus):
+       # should use sudo here
+       if vstatus.cur_selection == '55':
+               vstatus.mk.set_message('OPENSESAME')
+               logging.info('dispensing a door for %s'%vstatus.username)
+               if geteuid() == 0:
+                       #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
+                       ret = os.system('dispense -u "%s" door'%vstatus.username)
+               else:
+                       ret = os.system('dispense door')
+               if ret == 0:
+                       logging.info('door opened')
+                       vstatus.mk.set_message(center('DOOR OPEN'))
+               else:
+                       logging.warning('user %s tried to dispense a bad door'%vstatus.username)
+                       vstatus.mk.set_message(center('BAD DOOR'))
+               sleep(1)
+       elif vstatus.cur_selection == '81':
+               cookie(v)
+       elif vstatus.cur_selection == '99':
+               scroll_options(vstatus.username, vstatus.mk)
+               vstatus.cur_selection = ''
+               return
+       elif vstatus.cur_selection[1] == '8':
+               v.display('GOT DRINK?')
+               if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
+                       v.display('SEEMS NOT')
+               else:
+                       v.display('GOT DRINK!')
+       else:
+               # first see if it's a named slot
+               try:
+                       price, shortname, name = get_snack( vstatus.cur_selection )
+               except:
+                       price, shortname, name = get_snack( '--' )
+               dollarprice = "$%.2f" % ( price / 100.0 )
+               v.display(vstatus.cur_selection+' - %s'%dollarprice)
+               exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
+#              exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
+#              exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
+               if (exitcode == 0):
+                       # magic dispense syslog service
+                       syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
+                       v.vend(vstatus.cur_selection)
+                       v.display('THANK YOU')
+               elif (exitcode == 5):   # RV_BALANCE
+                       v.display('NO MONEY?')
+               elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
+                       v.display('EMPTY SLOT')
+               elif (exitcode == 1):   # RV_BADITEM (Dead slot)
+                       v.display('EMPTY SLOT')
+               else:
+                       syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
+                       v.display('UNK ERROR')
+       sleep(1)
+
+
+def price_check(v, vstatus):
+       if vstatus.cur_selection[1] == '8':
+               v.display(center('SEE COKE'))
+       else:
+               # first see if it's a named slot
+               try:
+                       price, shortname, name = get_snack( vstatus.cur_selection )
+               except:
+                       price, shortname, name = get_snack( '--' )
+               dollarprice = "$%.2f" % ( price / 100.0 )
+               v.display(vstatus.cur_selection+' - %s'%dollarprice)
+
+
+def handle_getting_pin_key(state, event, params, v, vstatus):
+       #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
+       key = params
+       if len(vstatus.cur_pin) < PIN_LENGTH:
+               if key == 11:
+                       if vstatus.cur_pin == '':
+                               vstatus.cur_user = ''
+                               reset_idler(v, vstatus)
+
+                               return
+                       vstatus.cur_pin = ''
+                       vstatus.mk.set_message('PIN: ')
+                       return
+               vstatus.cur_pin += chr(key + ord('0'))
+               vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
+               if len(vstatus.cur_pin) == PIN_LENGTH:
+                       vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
+                       if vstatus.username:
+                               v.beep(0, False)
+                               vstatus.cur_selection = ''
+                               vstatus.change_state(STATE_GET_SELECTION)
+                               scroll_options(vstatus.username, vstatus.mk, True)
+                               return
+                       else:
+                               v.beep(40, False)
+                               vstatus.mk.set_messages(
+                                       [(center('BAD PIN'), False, 1.0),
+                                        (center('SORRY'), False, 0.5)])
+                               vstatus.cur_user = ''
+                               vstatus.cur_pin = ''
+                       
+                               reset_idler(v, vstatus, 2)
+
+                               return
+
+
+def handle_getting_uid_key(state, event, params, v, vstatus):
+       #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
+       key = params
+
+       # complicated key handling here:
+
+       if len(vstatus.cur_user) == 0 and key == 9:
+               vstatus.cur_selection = ''
+               vstatus.time_to_autologout = None
+               vstatus.mk.set_message('PRICECHECK')
+               sleep(0.5)
+               scroll_options('', vstatus.mk)
+               vstatus.change_state(STATE_GET_SELECTION)
+               return
+
+       if len(vstatus.cur_user) <8:
+               if key == 11:
+                       vstatus.cur_user = ''
+
+                       reset_idler(v, vstatus)
+                       return
+               vstatus.cur_user += chr(key + ord('0'))
+               #logging.info('dob: '+vstatus.cur_user)
+               if len(vstatus.cur_user) > 5:
+                       vstatus.mk.set_message('>'+vstatus.cur_user)
+               else:
+                       vstatus.mk.set_message('UID: '+vstatus.cur_user)
+       
+       if len(vstatus.cur_user) == 5:
+               uid = int(vstatus.cur_user)
+
+               if uid == 0:
+                       logging.info('user '+vstatus.cur_user+' has a bad PIN')
+                       pfalken="""
+CARRIER DETECTED
+
+CONNECT 128000
+
+Welcome to Picklevision Sytems, Sunnyvale, CA
+
+Greetings Professor Falken.
+
+
+
+
+Shall we play a game?
+
+
+Please choose from the following menu:
+
+1. Tic-Tac-Toe
+2. Chess
+3. Checkers
+4. Backgammon
+5. Poker
+6. Toxic and Biochemical Warfare
+7. Global Thermonuclear War
+
+7 [ENTER]
+
+Wouldn't you prefer a nice game of chess?
+
+""".replace('\n','    ')
+                       vstatus.mk.set_messages([(pfalken, False, 10)])
+                       vstatus.cur_user = ''
+                       vstatus.cur_pin = ''
+                       
+                       reset_idler(v, vstatus, 10)
+
+                       return
+
+               if not has_good_pin(uid):
+                       logging.info('user '+vstatus.cur_user+' has a bad PIN')
+                       vstatus.mk.set_messages(
+                               [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
+                       vstatus.cur_user = ''
+                       vstatus.cur_pin = ''
+                       
+                       reset_idler(v, vstatus, 3)
+
+                       return
+
+
+               vstatus.cur_pin = ''
+               vstatus.mk.set_message('PIN: ')
+               logging.info('need pin for user %s'%vstatus.cur_user)
+               vstatus.change_state(STATE_GETTING_PIN)
+               return
+
+
+def handle_idle_key(state, event, params, v, vstatus):
+       #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
+
+       key = params
+
+       if key == 11:
+               vstatus.cur_user = ''
+               reset_idler(v, vstatus)
+               return
+       
+       vstatus.change_state(STATE_GETTING_UID)
+       run_handler(event, key, v, vstatus)
+
+
+def handle_idle_tick(state, event, params, v, vstatus):
+       ### State idling
+       if vstatus.mk.done():
+               idle_step(vstatus)
+
+       if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
+               vstatus.time_of_next_idler = time() + 30
+               choose_idler()
+       
+       ###
+
+       vstatus.mk.update_display()
+
+       vstatus.change_state(STATE_GRANDFATHER_CLOCK)
+       run_handler(event, params, v, vstatus)
+       sleep(0.05)
+
+def beep_on(when, before=0):
+       start = int(when - before)
+       end = int(when)
+       now = int(time())
+
+       if now >= start and now <= end:
+               return 1
+       return 0
+
+def handle_idle_grandfather_tick(state, event, params, v, vstatus):
+       ### check for interesting times
+       now = localtime()
+
+       quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
+       halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
+       threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
+       fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+
+       hourfromnow = localtime(time() + 3600)
+       
+       #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
+       onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+               0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+
+       ## check for X seconds to the hour
+       ## if case, update counter to 2
+       if beep_on(onthehour,15) \
+               or beep_on(halfhour,0) \
+               or beep_on(quarterhour,0) \
+               or beep_on(threequarterhour,0) \
+               or beep_on(fivetothehour,0):
+               vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
+               run_handler(event, params, v, vstatus)
+       else:
+               vstatus.change_state(STATE_IDLE)
+
+def handle_grandfather_tick(state, event, params, v, vstatus):
+       go_idle = 1
+
+       msg = []
+       ### we live in interesting times
+       now = localtime()
+
+       quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
+       halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
+       threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
+       fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+
+       hourfromnow = localtime(time() + 3600)
+       
+#      onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
+       onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+               0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+
+
+       #print "when it fashionable to wear a onion on your hip"
+
+       if beep_on(onthehour,15):
+               go_idle = 0
+               next_hour=((hourfromnow[3] + 11) % 12) + 1
+               if onthehour - time() < next_hour and onthehour - time() > 0:
+                       v.beep(0, False)
+
+                       t = int(time())
+                       if (t % 2) == 0:
+                               msg.append(("DING!", False, None))
+                       else:
+                               msg.append(("     DING!", False, None))
+               elif int(onthehour - time()) == 0:
+                       v.beep(255, False)
+                       msg.append(("   BONG!", False, None))
+                       msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
+       elif beep_on(halfhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" HALFHOUR ", False, 50))
+       elif beep_on(quarterhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" QTR HOUR ", False, 50))
+       elif beep_on(threequarterhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" 3 QTR HR ", False, 50))
+       elif beep_on(fivetothehour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
+       else:
+               go_idle = 1
+       
+       ## check for X seconds to the hour
+
+       if len(msg):
+               vstatus.mk.set_messages(msg)
+               sleep(1)
+
+       vstatus.mk.update_display()
+       ## if no longer case, return to idle
+
+       ## change idler to be clock
+       if go_idle and vstatus.mk.done():
+               vstatus.change_state(STATE_IDLE,1)
+
+def handle_door_idle(state, event, params, v, vstatus):
+       def twiddle(clock,v,wise = 2):
+               if (clock % 4 == 0):
+                       v.display("-FEED  ME-")
+               elif (clock % 4 == 1+wise):
+                       v.display("\\FEED  ME/")
+               elif (clock % 4 == 2):
+                       v.display("-FEED  ME-")
+               elif (clock % 4 == 3-wise):
+                       v.display("/FEED  ME\\")
+
+       # don't care right now.
+       now = int(time())
+
+       if ((now % 60 % 2) == 0):
+               twiddle(now, v)
+       else:
+               twiddle(now, v, wise=0)
+
+
+def handle_door_event(state, event, params, v, vstatus):
+       if params == 0:  #door open
+               vstatus.change_state(STATE_DOOR_OPENING)
+               logging.warning("Entering open door mode")
+               v.display("-FEED  ME-")
+               #door_open_mode(v);
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+       elif params == 1:  #door closed
+               vstatus.change_state(STATE_DOOR_CLOSING)
+               reset_idler(v, vstatus, 3)
+
+               logging.warning('Leaving open door mode')
+               v.display("-YUM YUM!-")
+
+def handle_mifare_event(state, event, params, v, vstatus):
+       card_id = params
+       # Translate card_id into uid.
+       if card_id == None:
+               return
+
+       try:
+               vstatus.cur_user = get_uid(card_id)
+               logging.info('Mapped card id to uid %s'%vstatus.cur_user)
+               vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
+       except ValueError:
+               vstatus.username = None
+       if vstatus.username:
+               v.beep(0, False)
+               vstatus.cur_selection = ''
+               vstatus.change_state(STATE_GET_SELECTION)
+               scroll_options(vstatus.username, vstatus.mk, True)
+               return
+       else:
+               v.beep(40, False)
+               vstatus.mk.set_messages(
+                       [(center('BAD CARD'), False, 1.0),
+                        (center('SORRY'), False, 0.5)])
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+       
+               reset_idler(v, vstatus, 2)
+               return
+
+def handle_mifare_add_user_event(state, event, params, v, vstatus):
+       card_id = params
+
+       # Translate card_id into uid.
+       if card_id == None:
+               return
+
+       try:
+               if get_uid(card_id) != None:
+                       vstatus.mk.set_messages(
+                               [(center('ALREADY'), False, 0.5),
+                                (center('ENROLLED'), False, 0.5)])
+
+                       # scroll_options(vstatus.username, vstatus.mk)
+                       return
+       except ValueError:
+               pass
+
+       logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
+       set_card_id(vstatus.cur_user, card_id)
+       vstatus.mk.set_messages(
+               [(center('CARD'), False, 0.5),
+                (center('ENROLLED'), False, 0.5)])
+
+       # scroll_options(vstatus.username, vstatus.mk)
+
+def return_to_idle(state,event,params,v,vstatus):
+       reset_idler(v, vstatus)
+
+def create_state_table(vstatus):
+       vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
+       vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
+       vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
+       vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
+
+       vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
+       vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
+       vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
+       vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
+
+       vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
+       vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
+       vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
+       vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
+
+       vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
+       vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
+       vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
+
+       vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
+       vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
+       vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
+
+       vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
+       vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
+       vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
+
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
+
+def get_state_table_handler(vstatus, state, event, counter):
+       return vstatus.state_table[(state,event,counter)]
+
+def time_to_next_update(vstatus):
+       idle_update = vstatus.time_of_next_idlestep - time()
+       if not vstatus.mk.done() and vstatus.mk.next_update is not None:
+               mk_update = vstatus.mk.next_update - time()
+               if mk_update < idle_update:
+                       idle_update = mk_update
+       return idle_update
+
+def run_forever(rfh, wfh, options, cf):
+       v = VendingMachine(rfh, wfh, USE_MIFARE)
+       vstatus = VendState(v)
+       create_state_table(vstatus)
+
+       logging.debug('PING is ' + str(v.ping()))
+
+       if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
+
+       setup_idlers(v)
+       reset_idler(v, vstatus)
+
+       # This main loop was hideous and the work of the devil.
+       # This has now been fixed (mostly) - mtearle
+       #
+       #
+       # notes for later surgery
+       #   (event, counter, ' ')
+       #        V
+       #   d[      ] = (method)
+       #
+       # ( return state - not currently implemented )
+
+       while True:
+               if USE_DB:
+                       try:
+                               db.handle_events()
+                       except DispenseDatabaseException, e:
+                               logging.error('Database error: '+str(e))
+
+               timeout = time_to_next_update(vstatus)
+               e = v.next_event(timeout)
+               (event, params) = e
+
+               run_handler(event, params, v, vstatus)
+
+#              logging.debug('Got event: ' + repr(e))
+
+
+def run_handler(event, params, v, vstatus):
+       handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
+       if handler:
+               handler(vstatus.state, event, params, v, vstatus)
+
+def connect_to_vend(options, cf):
+
+       if options.use_lat:
+               logging.info('Connecting to vending machine using LAT')
+               latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
+               rfh, wfh = latclient.get_fh()
+       elif options.use_serial:
+               # Open vending machine via serial.
+               logging.info('Connecting to vending machine using serial')
+               serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
+               rfh,wfh = serialclient.get_fh()
        else:
                #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
+               logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
                import socket
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-               sock.connect(('localhost', 5150))
+               sock.connect((options.host, options.port))
                rfh = sock.makefile('r')
                wfh = sock.makefile('w')
-       v = VendingMachine(rfh, wfh)
-       print 'PING is', v.ping()
+               global USE_MIFARE
+               USE_MIFARE = 0
+               
+       return rfh, wfh
+
+def parse_args():
+       from optparse import OptionParser
+
+       op = OptionParser(usage="%prog [OPTION]...")
+       op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
+       op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
+       op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
+       op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
+       op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
+       op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
+       op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
+       op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
+       op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
+       op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
+       op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
+       op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
+       options, args = op.parse_args()
+
+       if len(args) != 0:
+               op.error('extra command line arguments: ' + ' '.join(args))
+
+       return options
 
-       db = DispenseDatabase(v, DBServer, DBName, DBUser, DBPassword)
-       cur_user = ''
-       cur_pin = ''
-       cur_selection = ''
+config_options = {
+       'DBServer': ('Database', 'Server'),
+       'DBName': ('Database', 'Name'),
+       'DBUser': ('VendingMachine', 'DBUser'),
+       'DBPassword': ('VendingMachine', 'DBPassword'),
+       
+       'ServiceName': ('VendingMachine', 'ServiceName'),
+       'ServicePassword': ('VendingMachine', 'Password'),
+       
+       'ServerName': ('DecServer', 'Name'),
+       'ConnectPassword': ('DecServer', 'ConnectPassword'),
+       'PrivPassword': ('DecServer', 'PrivPassword'),
+       }
 
-       mk = MessageKeeper(v)
-       mk.set_message(GREETING)
-       logout_timeout = None
-       last_timeout_refresh = None
+class VendConfigFile:
+       def __init__(self, config_file, options):
+               try:
+                       cp = ConfigParser.ConfigParser()
+                       cp.read(config_file)
+
+                       for option in options:
+                               section, name = options[option]
+                               value = cp.get(section, name)
+                               self.__dict__[option] = value
+               
+               except ConfigParser.Error, e:
+                       raise SystemExit("Error reading config file "+config_file+": " + str(e))
+
+def create_pid_file(name):
+       try:
+               pid_file = file(name, 'w')
+               pid_file.write('%d\n'%os.getpid())
+               pid_file.close()
+       except IOError, e:
+               logging.warning('unable to write to pid file '+name+': '+str(e))
+
+def set_stuff_up():
+       def do_nothing(signum, stack):
+               signal.signal(signum, do_nothing)
+       def stop_server(signum, stack): raise KeyboardInterrupt
+       signal.signal(signal.SIGHUP, do_nothing)
+       signal.signal(signal.SIGTERM, stop_server)
+       signal.signal(signal.SIGINT, stop_server)
+
+       options = parse_args()
+       config_opts = VendConfigFile(options.config_file, config_options)
+       if options.daemon: become_daemon()
+       set_up_logging(options)
+       if options.pid_file != '': create_pid_file(options.pid_file)
+
+       return options, config_opts
 
+def clean_up_nicely(options, config_opts):
+       if options.pid_file != '':
+               try:
+                       os.unlink(options.pid_file)
+                       logging.debug('Removed pid file '+options.pid_file)
+               except OSError: pass  # if we can't delete it, meh
+
+def set_up_logging(options):
+       logger = logging.getLogger()
+       
+       if not options.daemon:
+               stderr_logger = logging.StreamHandler(sys.stderr)
+               stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
+               logger.addHandler(stderr_logger)
+       
+       if options.log_file != '':
+               try:
+                       file_logger = logging.FileHandler(options.log_file)
+                       file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
+                       logger.addHandler(file_logger)
+               except IOError, e:
+                       logger.warning('unable to write to log file '+options.log_file+': '+str(e))
+
+       if options.syslog != None:
+               sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
+               sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
+               logger.addHandler(sys_logger)
+
+       if options.quiet:
+               logger.setLevel(logging.WARNING)
+       elif options.verbose:
+               logger.setLevel(logging.DEBUG)
+       else:
+               logger.setLevel(logging.INFO)
+
+def become_daemon():
+       dev_null = file('/dev/null')
+       fd = dev_null.fileno()
+       os.dup2(fd, 0)
+       os.dup2(fd, 1)
+       os.dup2(fd, 2)
+       try:
+               if os.fork() != 0:
+                       sys.exit(0)
+               os.setsid()
+       except OSError, e:
+               raise SystemExit('failed to fork: '+str(e))
+
+def do_vend_server(options, config_opts):
        while True:
-               db.handle_events()
-
-               if logout_timeout != None:
-                       time_left = logout_timeout - time()
-                       if time_left < 10 and last_timeout_refresh > time_left:
-                               mk.set_message('LOGOUT: '+str(int(time_left)))
-                               last_timeout_refresh = int(time_left)
-
-               if logout_timeout != None and logout_timeout - time() <= 0:
-                       logout_timeout = None
-                       cur_user = ''
-                       cur_pin = ''
-                       cur_selection = ''
-                       mk.set_message(GREETING)
-
-               mk.update_display()
-
-               e = v.next_event(0)
-               if e == None:
-                       e = v.next_event(0.1)
-                       if e == None:
-                               continue
-               (event, params) = e
-               print e
-               if event == DOOR:
-                       if params == 0:
-                               door_open_mode(v);
-                               cur_user = ''
-                               cur_pin = ''
-                               mk.set_message(GREETING)
-               elif event == SWITCH:
-                       # don't care right now.
-                       pass
-               elif event == KEY:
-                       key = params
-                       # complicated key handling here:
-                       if len(cur_user) < 5:
-                               if key == 11:
-                                       cur_user = ''
-                                       mk.set_message(GREETING)
-                                       continue
-                               cur_user += chr(key + ord('0'))
-                               mk.set_message('UID: '+cur_user)
-                               if len(cur_user) == 5:
-                                       uid = int(cur_user)
-                                       if not has_good_pin(uid):
-                                               mk.set_messages(
-                                                       [(center('INVALID'), False, 0.7),
-                                                        (center('PIN'), False, 0.7),
-                                                        (center('SETUP'), False, 1.0),
-                                                        (GREETING, False, None)])
-                                               cur_user = ''
-                                               cur_pin = ''
-                                               continue
-                                       cur_pin = ''
-                                       mk.set_message('PIN: ')
-                                       continue
-                       elif len(cur_pin) < PIN_LENGTH:
-                               if key == 11:
-                                       if cur_pin == '':
-                                               cur_user = ''
-                                               mk.set_message(GREETING)
-                                               continue
-                                       cur_pin = ''
-                                       mk.set_message('PIN: ')
-                                       continue
-                               cur_pin += chr(key + ord('0'))
-                               mk.set_message('PIN: '+'X'*len(cur_pin))
-                               if len(cur_pin) == PIN_LENGTH:
-                                       username = verify_user_pin(int(cur_user), int(cur_pin))
-                                       if username:
-                                               v.beep(0, False)
-                                               cur_selection = ''
-
-                                               msg = [(center('WELCOME'), False, 0.8),
-                                                          (center(username), False, 0.9)]
-                                               msg.append(('CHOICES :', True, 1))
-                                               msg.append(('55 - DOOR', True, 1))
-                                               msg.append(('OR A SNACK', True, 1))
-                                               mk.set_messages(msg)
-                                               continue
-                                       else:
-                                               v.beep(40, False)
-                                               mk.set_messages(
-                                                       [(center('BAD PIN'), False, 1.0),
-                                                        (center('SORRY'), False, 0.5),
-                                                        (GREETING, False, None)])
-                                               cur_user = ''
-                                               cur_pin = ''
-                                               continue
-                       elif len(cur_selection) == 0:
-                               if key == 11:
-                                       cur_pin = ''
-                                       cur_user = ''
-                                       cur_selection = ''
-                                       v.display('BYE!')
-                                       sleep(0.5)
-                                       mk.set_message(GREETING)
-                                       continue
-                               cur_selection += chr(key + ord('0'))
-                               mk.set_message('SELECT: '+cur_selection)
-                       elif len(cur_selection) == 1:
-                               if key == 11:
-                                       cur_selection = ''
-                                       mk.set_message('SELECT: ')
-                                       continue
-                               else:
-                                       cur_selection += chr(key + ord('0'))
-                                       #make_selection(cur_selection)
-                                       # XXX this should move somewhere else:
-                                       if cur_selection == '55':
-                                               v.display('GOT DOOR?')
-                                               os.system('su - "%s" -c "dispense door"'%username)
-                                       elif cur_selection[1] == '8':
-                                               v.display('GOT COKE?')
-                                               os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
-                                       else:
-                                               v.display('HERES A '+cur_selection)
-                                               v.vend(cur_selection)
-                                       sleep(0.5)
-                                       v.display('THANK YOU')
-                                       sleep(0.5)
-                                       cur_selection = ''
-                                       logout_timeout = time() + 10
+               try:
+                       rfh, wfh = connect_to_vend(options, config_opts)
+               except (SerialClientException, socket.error), e:
+                       (exc_type, exc_value, exc_traceback) = sys.exc_info()
+                       del exc_traceback
+                       logging.error("Connection error: "+str(exc_type)+" "+str(e))
+                       logging.info("Trying again in 5 seconds.")
+                       sleep(5)
+                       continue
+               
+#              run_forever(rfh, wfh, options, config_opts)
+               
+               try:
+                       run_forever(rfh, wfh, options, config_opts)
+               except VendingException:
+                       logging.error("Connection died, trying again...")
+                       logging.info("Trying again in 5 seconds.")
+                       sleep(5)
+
+if __name__ == '__main__':
+       options, config_opts = set_stuff_up()
+       while True:
+               try:
+                       logging.warning('Starting Vend Server')
+                       do_vend_server(options, config_opts)
+                       logging.error('Vend Server finished unexpectedly, restarting')
+               except KeyboardInterrupt:
+                       logging.info("Killed by signal, cleaning up")
+                       clean_up_nicely(options, config_opts)
+                       logging.warning("Vend Server stopped")
+                       break
+               except SystemExit:
+                       break
+               except:
+                       (exc_type, exc_value, exc_traceback) = sys.exc_info()
+                       tb = format_tb(exc_traceback, 20)
+                       del exc_traceback
+                       
+                       logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
+                       logging.critical("Message: " + str(exc_value))
+                       logging.critical("Traceback:")
+                       for event in tb:
+                               for line in event.split('\n'):
+                                       logging.critical('    '+line)
+                       logging.critical("This message should be considered a bug in the Vend Server.")
+                       logging.critical("Please report this to someone who can fix it.")
+                       sleep(10)
+                       logging.warning("Trying again anyway (might not help, but hey...)")
+

UCC git Repository :: git.ucc.asn.au