Add SIGUSR1 traceback (made by jimbo, committed by TPG)
[uccvend-vendserver.git] / VendServer / VendServer.py
index 6a7af6b..cf9c8bb 100755 (executable)
@@ -1,14 +1,12 @@
 #!/usr/bin/python
-# vim:ts=4
+# vim: ts=4 sts=4 sw=4 noexpandtab
 
-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 subprocess import Popen, PIPE
 from LATClient import LATClient, LATClientException
@@ -21,8 +19,8 @@ from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,Fort
 from SnackConfig import get_snack#, get_snacks
 import socket
 from posix import geteuid
-from LDAPConnector import get_uid,get_uname, set_card_id
 from OpenDispense import OpenDispense as Dispense
+import TracebackPrinter
 
 CREDITS="""
 This vending machine software brought to you by:
@@ -95,36 +93,6 @@ class VendConfigFile:
                except ConfigParser.Error, e:
                        raise SystemExit("Error reading config file "+config_file+": " + str(e))
 
-class DispenseDatabaseException(Exception): pass
-
-class DispenseDatabase:
-       def __init__(self, vending_machine, host, name, user, password):
-               self.vending_machine = vending_machine
-               self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
-               self.db.query('LISTEN vend_requests')
-
-       def process_requests(self):
-               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:
-                       raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
-               for (id, slot) in outstanding:
-                       (worked, code, string) = self.vending_machine.vend(slot)
-                       logging.debug (str((worked, code, string)))
-                       if worked:
-                               query = 'SELECT vend_success(%s)'%id
-                               self.db.query(query).getresult()
-                       else:
-                               query = 'SELECT vend_failed(%s)'%id
-                               self.db.query(query).getresult()
-
-       def handle_events(self):
-               notifier = self.db.getnotify()
-               while notifier is not None:
-                       self.process_requests()
-                       notify = self.db.getnotify()
 """
 This class manages the current state of the vending machine.
 """
@@ -164,38 +132,32 @@ class VendServer():
        _pin_pin = '----'
        _last_card_id = -1
 
+       dispense = None
+
        """
        Show information to the user as to what can be dispensed.
        """
        def scroll_options(self, username, mk, welcome = False):
                # If the user has just logged in, show them their balance
                if welcome:
-                       # Balance checking
-                       acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
-                       # this is fucking appalling
-                       balance = acct[acct.find("$")+1:acct.find("(")].strip()
-                       
+                       balance = self.dispense.getBalance()
                        msg = [(self.center('WELCOME'), False, TEXT_SPEED),
-                                  (self.center(username), False, TEXT_SPEED),
+                                  (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
                                   (self.center(balance), False, TEXT_SPEED),]
                else:
                        msg = []
                choices = ' '*10+'CHOICES: '
 
                # Show what is in the coke machine
+               # Need to update this so it uses the abstracted system
                cokes = []
-               for i in range(0, 7):
-                       args = ('dispense', 'iteminfo', 'coke:%i' % i)
-                       info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
-                       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 i in ['08', '18', '28', '38', '48', '58', '68']:
+                       cokes.append((i, self.dispense.getItemInfo(i)))
 
                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)
+                       if c[1][0] == 'dead':
+                               continue
+                       choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
 
                # Show the final few options
                choices += '55-DOOR '
@@ -206,88 +168,6 @@ class VendServer():
                # Send it to the display
                mk.set_messages(msg)
 
-
-       """
-       Verify the users pin
-       """
-       def _check_pin(self, uid, pin):
-               print "_check_pin('",uid,"',---)"
-               if uid != self._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:
-                               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
-                       self._pin_uid = uid
-                       self._pin_pin = pinstr
-                       self._pin_uname = info.pw_name
-               else:
-                       pinstr = self._pin_pin
-               if pin == int(pinstr):
-                       logging.info("Pin correct for %d",uid)
-               else:
-                       logging.info("Pin incorrect for %d",uid)
-               return pin == int(pinstr)
-
-       """
-       Check if the users account has been disabled
-       """
-       def acct_is_disabled(self, name=None):
-               if name == None:
-                       name = self._pin_uname
-               acct, unused = Popen(['dispense', 'acct', self._pin_uname], close_fds=True, stdout=PIPE).communicate()
-               # this is fucking appalling
-               flags = acct[acct.find("(")+1:acct.find(")")].strip()
-               if 'disabled' in flags:
-                       return True
-               if 'internal' in flags:
-                       return True
-               return False
-
-       """
-       Check that the user has a valid pin set
-       """
-       def has_good_pin(self, uid):
-               return self._check_pin(uid, None) != None
-
-       """
-       Verify the users pin.
-       """
-       def verify_user_pin(self, uid, pin, skip_pin_check=False):
-               if skip_pin_check or self._check_pin(uid, pin) == True:
-                       info = pwd.getpwuid(uid)
-                       if skip_pin_check:
-                               if self.acct_is_disabled(info.pw_name):
-                                       logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
-                                       return '-disabled-'
-                               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
-
        """
        In here just for fun.
        """
@@ -455,12 +335,14 @@ class VendServer():
                                self.vstatus.last_timeout_refresh = int(time_left)
                                self.vstatus.cur_selection = ''
 
+               # Login timed out: Log out the current user.
                if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
                        self.vstatus.time_to_autologout = None
                        self.vstatus.cur_user = ''
                        self.vstatus.cur_pin = ''
                        self.vstatus.cur_selection = ''
                        self._last_card_id = -1
+                       self.dispense.logOut()
                        self.reset_idler()
 
                ### State fully logged out ... reset variables
@@ -489,7 +371,8 @@ class VendServer():
                                self.vstatus.cur_pin = ''
                                self.vstatus.cur_user = ''
                                self.vstatus.cur_selection = ''
-                               _last_card_id = -1
+                               self._last_card_id = -1
+                               self.dispense.logOut()
                                self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
                                self.reset_idler(2)
                                return
@@ -500,18 +383,22 @@ class VendServer():
                        if key == 11:
                                self.vstatus.cur_selection = ''
                                self.vstatus.time_to_autologout = None
+                               self.dispense.logOut()
                                self.scroll_options(self.vstatus.username, self.vstatus.mk)
                                return
                        else:
                                self.vstatus.cur_selection += chr(key + ord('0'))
-                               if self.vstatus.cur_user:
+                               if self.dispense.isLoggedIn():
                                        self.make_selection()
                                        self.vstatus.cur_selection = ''
                                        self.vstatus.time_to_autologout = time() + 8
                                        self.vstatus.last_timeout_refresh = None
                                else:
                                        # Price check mode.
-                                       self.price_check()
+                                       (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
+                                       dollarprice = "$%.2f" % ( price / 100.0 )
+                                       self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
+
                                        self.vstatus.cur_selection = ''
                                        self.vstatus.time_to_autologout = None
                                        self.vstatus.last_timeout_refresh = None
@@ -520,6 +407,7 @@ class VendServer():
        Triggered when the user has entered the id of something they would like to purchase.
        """
        def make_selection(self):
+               logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
                # should use sudo here
                if self.vstatus.cur_selection == '55':
                        self.vstatus.mk.set_message('OPENSESAME')
@@ -558,12 +446,13 @@ class VendServer():
                        exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.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, self.vstatus.cur_selection, self.vstatus.username))
                                (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
                                if worked:
                                        self.v.display('THANK YOU')
+                                       syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
                                else:
                                        print "Vend Failed:", code, string
+                                       syslog.syslog(syslog.LOG_WARNING | syslog.LOG_LOCAL4, "vending %s (slot %s) for %s FAILED %r %r" % (name, self.vstatus.cur_selection, self.vstatus.username, code, string))
                                        self.v.display('VEND FAIL')
                        elif (exitcode == 5):   # RV_BALANCE
                                self.v.display('NO MONEY?')
@@ -576,23 +465,6 @@ class VendServer():
                                self.v.display('UNK ERROR')
                sleep(1)
 
-       """
-       Find the price of an item.
-       """
-       def price_check():
-               if self.vstatus.cur_selection[1] == '8':
-                       args = ('dispense', 'iteminfo', 'coke:' + self.vstatus.cur_selection[0])
-                       info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
-                       dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
-               else:
-                       # first see if it's a named slot
-                       try:
-                               price, shortname, name = get_snack( self.vstatus.cur_selection )
-                       except:
-                               price, shortname, name = get_snack( '--' )
-                       dollarprice = "$%.2f" % ( price / 100.0 )
-               self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
-
        """
        Triggered when the user presses a button while entering their pin.
        """
@@ -602,7 +474,8 @@ class VendServer():
                        if key == 11:
                                if self.vstatus.cur_pin == '':
                                        self.vstatus.cur_user = ''
-                                       slef.reset_idler()
+                                       self.dispense.logOut()
+                                       self.reset_idler()
 
                                        return
                                self.vstatus.cur_pin = ''
@@ -611,8 +484,8 @@ class VendServer():
                        self.vstatus.cur_pin += chr(key + ord('0'))
                        self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
                        if len(self.vstatus.cur_pin) == PIN_LENGTH:
-                               self.vstatus.username = self.verify_user_pin(int(self.vstatus.cur_user), int(self.vstatus.cur_pin))
-                               if self.vstatus.username:
+                               if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
+                                       self.vstatus.username = self.dispense.getUsername()
                                        self.v.beep(0, False)
                                        self.vstatus.cur_selection = ''
                                        self.vstatus.change_state(STATE_GET_SELECTION)
@@ -649,6 +522,7 @@ class VendServer():
                if len(self.vstatus.cur_user) <8:
                        if key == 11:
                                self.vstatus.cur_user = ''
+                               self.dispense.logOut()
 
                                self.reset_idler()
                                return
@@ -702,18 +576,9 @@ class VendServer():
 
                                return
 
-                       if not self.has_good_pin(uid):
-                               logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
-                               self.vstatus.mk.set_messages(
-                                       [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
-                               self.vstatus.cur_user = ''
-                               self.vstatus.cur_pin = ''
-                               
-                               self.reset_idler(3)
-
-                               return
-                       
-                       if self.acct_is_disabled():
+                       # TODO Fix this up do we can check before logging in
+                       """
+                       if self.dispense.isDisabled():
                                logging.info('user '+self.vstatus.cur_user+' is disabled')
                                self.vstatus.mk.set_messages(
                                        [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
@@ -722,7 +587,7 @@ class VendServer():
                                
                                self.reset_idler(3)
                                return
-
+                       """
 
                        self.vstatus.cur_pin = ''
                        self.vstatus.mk.set_message('PIN: ')
@@ -737,6 +602,7 @@ class VendServer():
                key = params
                if key == 11:
                        self.vstatus.cur_user = ''
+                       self.dispense.logOut()
                        self.reset_idler()
                        return
                
@@ -901,6 +767,7 @@ class VendServer():
                        logging.warning("Entering open door mode")
                        self.v.display("-FEED  ME-")
                        #door_open_mode(v);
+                       self.dispense.logOut()
                        self.vstatus.cur_user = ''
                        self.vstatus.cur_pin = ''
                elif params == 1:  #door closed
@@ -921,47 +788,39 @@ class VendServer():
 
                self._last_card_id = card_id
                
-               try:
-                       self.vstatus.cur_user = get_uid(card_id)
-                       logging.info('Mapped card id to uid %s'%self.vstatus.cur_user)
-                       self.vstatus.username = get_uname(self.vstatus.cur_user)
-                       if self.acct_is_disabled(self.vstatus.username):
-                               self.vstatus.username = '-disabled-'
-               except ValueError:
-                       self.vstatus.username = None
-               if self.vstatus.username == '-disabled-':
+               if not self.dispense.authMifareCard(card_id):
                        self.v.beep(40, False)
                        self.vstatus.mk.set_messages(
-                               [(self.center('ACCT DISABLED'), False, 1.0),
+                               [(self.center('BAD CARD'), False, 1.0),
                                 (self.center('SORRY'), False, 0.5)])
                        self.vstatus.cur_user = ''
                        self.vstatus.cur_pin = ''
-                       self.vstatus.username = None
+                       self._last_card_id = -1
                
                        self.reset_idler(2)
                        return
-               elif self.vstatus.username:
-                       self.v.beep(0, False)
-                       self.vstatus.cur_selection = ''
-                       self.vstatus.change_state(STATE_GET_SELECTION)
-                       self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
-                       return
-               else:
+               elif self.dispense.isDisabled():
+                       logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
                        self.v.beep(40, False)
                        self.vstatus.mk.set_messages(
-                               [(self.center('BAD CARD'), False, 1.0),
+                               [(self.center('ACCT DISABLED'), False, 1.0),
                                 (self.center('SORRY'), False, 0.5)])
-                       self.vstatus.cur_user = ''
-                       self.vstatus.cur_pin = ''
-                       self._last_card_id = -1
-               
+                       self.dispense.logOut()
                        self.reset_idler(2)
                        return
+               else:
+                       logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
+                       self.vstatus.cur_user = '----'
+                       self.vstatus.username = self.dispense.getUsername()
+                       self.vstatus.cur_selection = ''
+                       self.vstatus.change_state(STATE_GET_SELECTION)
+                       self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
+                       return
 
        """
        Triggered when a user swipes their card and the machine is logged in.
        """
-       def handle_mifare_add_user_event(self, evnt, params):
+       def handle_mifare_add_user_event(self, event, params):
                card_id = params
 
                # Translate card_id into uid.
@@ -969,25 +828,15 @@ class VendServer():
                        return
 
                self._last_card_id = card_id
-               
-               try:
-                       if get_uid(card_id) != None:
-                               self.vstatus.mk.set_messages(
-                                       [(self.center('ALREADY'), False, 0.5),
-                                        (self.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, self.vstatus.cur_user, self.vstatus.username))
-               self.set_card_id(self.vstatus.cur_user, self.card_id)
-               self.vstatus.mk.set_messages(
-                       [(self.center('CARD'), False, 0.5),
-                        (self.center('ENROLLED'), False, 0.5)])
 
-               # scroll_options(vstatus.username, vstatus.mk)
+               if not self.dispense.addCard(card_id):
+                       self.vstatus.mk.set_messages(
+                               [(self.center('ALREADY'), False, 0.5),
+                                (self.center('ENROLLED'), False, 0.5)])
+               else:
+                       self.vstatus.mk.set_messages(
+                               [(self.center('CARD'), False, 0.5),
+                                (self.center('ENROLLED'), False, 0.5)])
 
        def return_to_idle(self, event, params):
                self.reset_idler()
@@ -1050,23 +899,16 @@ class VendServer():
 
        def run_forever(self, rfh, wfh, options, cf):
                self.v = VendingMachine(rfh, wfh, USE_MIFARE)
+               self.dispense = Dispense()
                self.vstatus = VendState(self.v)
                self.create_state_table()
 
                logging.debug('PING is ' + str(self.v.ping()))
 
-               if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
-
                self.setup_idlers()
                self.reset_idler()
 
                while True:
-                       if USE_DB:
-                               try:
-                                       db.handle_events()
-                               except DispenseDatabaseException, e:
-                                       logging.error('Database error: '+str(e))
-
                        timeout = self.time_to_next_update()
                        (event, params) = self.v.next_event(timeout)
                        self.run_handler(event, params)
@@ -1122,6 +964,7 @@ def parse_args():
        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')
+        op.add_option('--traceback-file', dest='traceback_file', default='', help='destination to print tracebacks when receiving SIGUSR1')
        options, args = op.parse_args()
 
        if len(args) != 0:
@@ -1150,7 +993,7 @@ def set_stuff_up():
        if options.daemon: become_daemon()
        set_up_logging(options)
        if options.pid_file != '': create_pid_file(options.pid_file)
-
+       if options.traceback_file != '': TracebackPrinter.traceback_init(options.traceback_file)
        return options, config_opts
 
 def clean_up_nicely(options, config_opts):
@@ -1214,7 +1057,7 @@ def do_vend_server(options, config_opts):
                        continue
                
 #              run_forever(rfh, wfh, options, config_opts)
-               
+
                try:
                        vserver = VendServer()
                        vserver.run_forever(rfh, wfh, options, config_opts)

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