add .gitignore
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
index fa5ba5b..2a9a55a 100755 (executable)
@@ -2,6 +2,7 @@
 # vim:ts=4
 
 USE_DB = 0
 # vim:ts=4
 
 USE_DB = 0
+USE_MIFARE = 1
 
 import ConfigParser
 import sys, os, string, re, pwd, signal, math, syslog
 
 import ConfigParser
 import sys, os, string, re, pwd, signal, math, syslog
@@ -9,7 +10,7 @@ import logging, logging.handlers
 from traceback import format_tb
 if USE_DB: import pg
 from time import time, sleep, mktime, localtime
 from traceback import format_tb
 if USE_DB: import pg
 from time import time, sleep, mktime, localtime
-from popen2 import popen2
+from subprocess import Popen, PIPE
 from LATClient import LATClient, LATClientException
 from SerialClient import SerialClient, SerialClientException
 from VendingMachine import VendingMachine, VendingException
 from LATClient import LATClient, LATClientException
 from SerialClient import SerialClient, SerialClientException
 from VendingMachine import VendingMachine, VendingException
@@ -17,9 +18,10 @@ 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 HorizScroll import HorizScroll
 from random import random, seed
 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
-from SnackConfig import get_snacks, get_snack
+from SnackConfig import get_snack#, get_snacks
 import socket
 from posix import geteuid
 import socket
 from posix import geteuid
+from LDAPConnector import get_uid,get_uname, set_card_id
 
 CREDITS="""
 This vending machine software brought to you by:
 
 CREDITS="""
 This vending machine software brought to you by:
@@ -56,7 +58,7 @@ STATE_GET_SELECTION,
 STATE_GRANDFATHER_CLOCK,
 ) = range(1,8)
 
 STATE_GRANDFATHER_CLOCK,
 ) = range(1,8)
 
-TEXT_SPEED = 0.6
+TEXT_SPEED = 0.8
 IDLE_SPEED = 0.05
 
 class DispenseDatabaseException(Exception): pass
 IDLE_SPEED = 0.05
 
 class DispenseDatabaseException(Exception): pass
@@ -92,10 +94,10 @@ class DispenseDatabase:
 
 def scroll_options(username, mk, welcome = False):
        if welcome:
 
 def scroll_options(username, mk, welcome = False):
        if welcome:
-           # Balance checking: crap code, [DAA]'s fault
-           acct = os.popen('dispense acct %s' % username)
-               balance = acct.read()[15:22]
-               acct.close()
+               # 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()
         
                msg = [(center('WELCOME'), False, TEXT_SPEED),
                           (center(username), False, TEXT_SPEED),
         
                msg = [(center('WELCOME'), False, TEXT_SPEED),
                           (center(username), False, TEXT_SPEED),
@@ -103,13 +105,16 @@ def scroll_options(username, mk, welcome = False):
        else:
                msg = []
        choices = ' '*10+'CHOICES: '
        else:
                msg = []
        choices = ' '*10+'CHOICES: '
-       try:
-               coke_machine = file('/home/other/coke/coke_contents')
-               cokes = coke_machine.readlines()
-               coke_machine.close()
-       except:
-               cokes = []
-               pass
+
+       # Get coke contents
+       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 c in cokes:
                c = c.strip()
                (slot_num, price, slot_name) = c.split(' ', 2)
        for c in cokes:
                c = c.strip()
                (slot_num, price, slot_name) = c.split(' ', 2)
@@ -133,41 +138,78 @@ def scroll_options(username, mk, welcome = False):
        msg.append((choices, False, None))
        mk.set_messages(msg)
 
        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:
-               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)
+_pin_uid = 0
+_pin_uname = 'root'
+_pin_pin = '----'
+
+def _check_pin(uid, pin):
+       global _pin_uid
+       global _pin_uname
+       global _pin_pin
+       print "_check_pin('",uid,"',---)"
+       if uid != _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
+               _pin_uid = uid
+               _pin_pin = pinstr
+               _pin_uname = info.pw_name
+       else:
+               pinstr = _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)
+
+def acct_is_disabled(name=None):
+       global _pin_uname
+       if name == None:
+               name = _pin_uname
+       acct, unused = Popen(['dispense', 'acct', _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
 
 def has_good_pin(uid):
 
 def has_good_pin(uid):
-       return get_pin(uid) != None
+       return _check_pin(uid, None) != None
 
 def verify_user_pin(uid, pin, skip_pin_check=False):
 
 def verify_user_pin(uid, pin, skip_pin_check=False):
-       if skip_pin_check or get_pin(uid) == pin:
+       if skip_pin_check or _check_pin(uid, pin) == True:
                info = pwd.getpwuid(uid)
                info = pwd.getpwuid(uid)
-               logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
+               if skip_pin_check:
+                       if 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 info.pw_name
        else:
                logging.info('refused pin for uid %d'%(uid))
@@ -230,6 +272,8 @@ def setup_idlers(v):
                ClockIdler(v),
                StringIdler(v),
                TrainIdler(v),
                ClockIdler(v),
                StringIdler(v),
                TrainIdler(v),
+               # "Hello World" in brainfuck
+               StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
                ]
        disabled = [
                ]
                ]
        disabled = [
                ]
@@ -402,7 +446,8 @@ def make_selection(v, vstatus):
                vstatus.mk.set_message('OPENSESAME')
                logging.info('dispensing a door for %s'%vstatus.username)
                if geteuid() == 0:
                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('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:
                else:
                        ret = os.system('dispense door')
                if ret == 0:
@@ -420,7 +465,7 @@ def make_selection(v, vstatus):
                return
        elif vstatus.cur_selection[1] == '8':
                v.display('GOT DRINK?')
                return
        elif vstatus.cur_selection[1] == '8':
                v.display('GOT DRINK?')
-               if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
+               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!')
                        v.display('SEEMS NOT')
                else:
                        v.display('GOT DRINK!')
@@ -432,21 +477,35 @@ def make_selection(v, vstatus):
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                v.display(vstatus.cur_selection+' - %s'%dollarprice)
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                v.display(vstatus.cur_selection+' - %s'%dollarprice)
-               exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
+#              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))
                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')
+                       (worked, code, string) = v.vend(vstatus.cur_selection)
+                       if worked:
+                               v.display('THANK YOU')
+                       else:
+                               print "Vend Failed:", code, string
+                               v.display('VEND FAIL')
+               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))
                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('NO MONEY?')
+                       v.display('UNK ERROR')
        sleep(1)
 
 
 def price_check(v, vstatus):
        if vstatus.cur_selection[1] == '8':
        sleep(1)
 
 
 def price_check(v, vstatus):
        if vstatus.cur_selection[1] == '8':
-               v.display(center('SEE COKE'))
+               args = ('dispense', 'iteminfo', 'coke:' + 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:
        else:
                # first see if it's a named slot
                try:
@@ -454,7 +513,7 @@ def price_check(v, vstatus):
                except:
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                except:
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
-               v.display(vstatus.cur_selection+' - %s'%dollarprice)
+       v.display(vstatus.cur_selection+' - %s'%dollarprice)
 
 
 def handle_getting_pin_key(state, event, params, v, vstatus):
 
 
 def handle_getting_pin_key(state, event, params, v, vstatus):
@@ -574,6 +633,16 @@ Wouldn't you prefer a nice game of chess?
                        reset_idler(v, vstatus, 3)
 
                        return
                        reset_idler(v, vstatus, 3)
 
                        return
+               
+               if acct_is_disabled():
+                       logging.info('user '+vstatus.cur_user+' is disabled')
+                       vstatus.mk.set_messages(
+                               [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
+                       vstatus.cur_user = ''
+                       vstatus.cur_pin = ''
+                       
+                       reset_idler(v, vstatus, 3)
+                       return
 
 
                vstatus.cur_pin = ''
 
 
                vstatus.cur_pin = ''
@@ -754,11 +823,31 @@ def handle_door_event(state, event, params, v, vstatus):
                v.display("-YUM YUM!-")
 
 def handle_mifare_event(state, event, params, v, vstatus):
                v.display("-YUM YUM!-")
 
 def handle_mifare_event(state, event, params, v, vstatus):
-       card_uid = params
+       card_id = params
        # Translate card_id into uid.
        # Translate card_id into uid.
-       vstatus.cur_user = str(card_uid)
-       vstatus.username = verify_user_pin(int(card_uid), None, True)
-       if vstatus.username:
+       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 = get_uname(vstatus.cur_user)
+               if acct_is_disabled(vstatus.username):
+                       vstatus.username = '-disabled-'
+       except ValueError:
+               vstatus.username = None
+       if vstatus.username == '-disabled-':
+               v.beep(40, False)
+               vstatus.mk.set_messages(
+                       [(center('ACCT DISABLED'), False, 1.0),
+                        (center('SORRY'), False, 0.5)])
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+               vstatus.username = None
+       
+               reset_idler(v, vstatus, 2)
+               return
+       elif vstatus.username:
                v.beep(0, False)
                vstatus.cur_selection = ''
                vstatus.change_state(STATE_GET_SELECTION)
                v.beep(0, False)
                vstatus.cur_selection = ''
                vstatus.change_state(STATE_GET_SELECTION)
@@ -775,6 +864,32 @@ def handle_mifare_event(state, event, params, v, vstatus):
                reset_idler(v, vstatus, 2)
                return
 
                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 return_to_idle(state,event,params,v,vstatus):
        reset_idler(v, vstatus)
 
@@ -807,7 +922,7 @@ def create_state_table(vstatus):
        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,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)] = do_nothing
+       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,TICK,1)] = handle_idle_grandfather_tick
        vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
@@ -829,7 +944,7 @@ def time_to_next_update(vstatus):
        return idle_update
 
 def run_forever(rfh, wfh, options, cf):
        return idle_update
 
 def run_forever(rfh, wfh, options, cf):
-       v = VendingMachine(rfh, wfh)
+       v = VendingMachine(rfh, wfh, USE_MIFARE)
        vstatus = VendState(v)
        create_state_table(vstatus)
 
        vstatus = VendState(v)
        create_state_table(vstatus)
 
@@ -891,6 +1006,8 @@ def connect_to_vend(options, cf):
                sock.connect((options.host, options.port))
                rfh = sock.makefile('r')
                wfh = sock.makefile('w')
                sock.connect((options.host, options.port))
                rfh = sock.makefile('r')
                wfh = sock.makefile('w')
+               global USE_MIFARE
+               USE_MIFARE = 0
                
        return rfh, wfh
 
                
        return rfh, wfh
 
@@ -898,7 +1015,7 @@ def parse_args():
        from optparse import OptionParser
 
        op = OptionParser(usage="%prog [OPTION]...")
        from optparse import OptionParser
 
        op = OptionParser(usage="%prog [OPTION]...")
-       op.add_option('-f', '--config-file', default='/etc/dispense/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
+       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('--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')
@@ -1029,6 +1146,8 @@ def do_vend_server(options, config_opts):
                        sleep(5)
                        continue
                
                        sleep(5)
                        continue
                
+#              run_forever(rfh, wfh, options, config_opts)
+               
                try:
                        run_forever(rfh, wfh, options, config_opts)
                except VendingException:
                try:
                        run_forever(rfh, wfh, options, config_opts)
                except VendingException:

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