#!/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
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:
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.
"""
_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 '
# 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.
"""
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
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
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
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')
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?')
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.
"""
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 = ''
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)
if len(self.vstatus.cur_user) <8:
if key == 11:
self.vstatus.cur_user = ''
+ self.dispense.logOut()
self.reset_idler()
return
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)])
self.reset_idler(3)
return
-
+ """
self.vstatus.cur_pin = ''
self.vstatus.mk.set_message('PIN: ')
key = params
if key == 11:
self.vstatus.cur_user = ''
+ self.dispense.logOut()
self.reset_idler()
return
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
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.
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()
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)
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:
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):
continue
# run_forever(rfh, wfh, options, config_opts)
-
+
try:
vserver = VendServer()
vserver.run_forever(rfh, wfh, options, config_opts)