7 import sys, os, string, re, pwd, signal, math
8 import logging, logging.handlers
9 from traceback import format_tb
11 from time import time, sleep
12 from popen2 import popen2
13 from LATClient import LATClient, LATClientException
14 from SerialClient import SerialClient, SerialClientException
15 from VendingMachine import VendingMachine, VendingException
16 from MessageKeeper import MessageKeeper
17 from HorizScroll import HorizScroll
18 from random import random, seed
19 from Idler import TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from posix import geteuid
24 This vending machine software brought to you by:
29 and a collective of hungry alpacas.
33 For a good time call +61 8 6488 3901
39 GREETING = 'UCC SNACKS'
49 STATE_DOOR_OPENING = 2
50 STATE_DOOR_CLOSING = 3
53 STATE_GET_SELECTION = 6
55 class DispenseDatabaseException(Exception): pass
57 class DispenseDatabase:
58 def __init__(self, vending_machine, host, name, user, password):
59 self.vending_machine = vending_machine
60 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
61 self.db.query('LISTEN vend_requests')
63 def process_requests(self):
64 logging.debug('database processing')
65 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
67 outstanding = self.db.query(query).getresult()
68 except (pg.error,), db_err:
69 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
70 for (id, slot) in outstanding:
71 (worked, code, string) = self.vending_machine.vend(slot)
72 logging.debug (str((worked, code, string)))
74 query = 'SELECT vend_success(%s)'%id
75 self.db.query(query).getresult()
77 query = 'SELECT vend_failed(%s)'%id
78 self.db.query(query).getresult()
80 def handle_events(self):
81 notifier = self.db.getnotify()
82 while notifier is not None:
83 self.process_requests()
84 notify = self.db.getnotify()
86 def scroll_options(username, mk, welcome = False):
88 msg = [(center('WELCOME'), False, 0.8),
89 (center(username), False, 0.8)]
92 choices = ' '*10+'CHOICES: '
94 coke_machine = file('/home/other/coke/coke_contents')
95 cokes = coke_machine.readlines()
102 (slot_num, price, slot_name) = c.split(' ', 2)
103 if slot_name == 'dead': continue
104 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
105 choices += '55-DOOR '
106 choices += 'OR A SNACK. '
107 choices += '99 TO READ AGAIN. '
108 choices += 'CHOICE? '
109 msg.append((choices, False, None))
114 info = pwd.getpwuid(uid)
116 logging.info('getting pin for uid %d: user not in password file'%uid)
118 if info.pw_dir == None: return False
119 pinfile = os.path.join(info.pw_dir, '.pin')
123 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
126 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
131 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
133 pinstr = f.readline()
135 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
136 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
140 def has_good_pin(uid):
141 return get_pin(uid) != None
143 def verify_user_pin(uid, pin):
144 if get_pin(uid) == pin:
145 info = pwd.getpwuid(uid)
146 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
149 logging.info('refused pin for uid %d'%(uid))
155 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
156 choice = int(random()*len(messages))
157 msg = messages[choice]
158 left = range(len(msg))
159 for i in range(len(msg)):
160 if msg[i] == ' ': left.remove(i)
164 for i in range(0, len(msg)):
170 s += chr(int(random()*26)+ord('A'))
179 return ' '*((LEN-len(str))/2)+str
190 StringIdler(v, text="Kill 'em all", repeat=False),
191 GrayIdler(v,one="*",zero="-"),
192 StringIdler(v, text=CREDITS),
193 GrayIdler(v,one="/",zero="\\"),
195 GrayIdler(v,one="X",zero="O"),
196 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
197 GrayIdler(v,one="*",zero="-",reorder=1),
198 StringIdler(v, text=str(math.pi) + " "),
200 GrayIdler(v,one="/",zero="\\",reorder=1),
201 StringIdler(v, text=str(math.e) + " "),
202 GrayIdler(v,one="X",zero="O",reorder=1),
203 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 - and order as Quinn - I am getting really hungry", repeat=False),
204 PipeIdler(v, "/usr/bin/ypcat", "passwd"),
212 idler = choose_idler()
219 iiindex = idlers.index(idler)
223 move = int(random()*len(idlers)) + 1
226 idler = idlers[( (iiindex + 1) % iilen)]
227 move = move - idler.affinity()
238 def __init__(self,v):
239 self.state_table = {}
240 self.state = STATE_IDLE
243 self.mk = MessageKeeper(v)
247 self.cur_selection = ''
248 self.time_to_autologout = None
250 self.time_to_idle = None
252 self.last_timeout_refresh = None
256 def handle_tick_event(event, params, v, vstatus):
257 # don't care right now.
260 def handle_switch_event(event, params, v, vstatus):
261 # don't care right now.
265 def do_nothing(state, event, params, v, vstatus):
266 print "doing nothing (s,e,p)", state, " ", event, " ", params
269 def handle_getting_uid_idle(state, event, params, v, vstatus):
270 # don't care right now.
273 def handle_getting_pin_idle(state, event, params, v, vstatus):
274 # don't care right now.
277 def handle_get_selection_idle(state, event, params, v, vstatus):
278 # don't care right now.
280 ### State logging out ..
281 if vstatus.time_to_autologout != None:
282 time_left = vstatus.time_to_autologout - time()
283 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
284 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
285 vstatus.last_timeout_refresh = int(time_left)
286 vstatus.cur_selection = ''
288 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
289 vstatus.time_to_autologout = None
290 vstatus.cur_user = ''
292 vstatus.cur_selection = ''
295 vstatus.state = STATE_IDLE
297 vstatus.mk.set_message(GREETING)
299 ### State fully logged out ... reset variables
300 if vstatus.time_to_autologout and not vstatus.mk.done():
301 vstatus.time_to_autologout = None
302 if vstatus.cur_user == '' and vstatus.time_to_autologout:
303 vstatus.time_to_autologout = None
306 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
308 vstatus.time_to_autologout = time() + 15
309 vstatus.last_timeout_refresh = None
311 ### State logged out ... after normal logout??
312 # perhaps when logged in?
313 if vstatus.time_to_idle is not None and vstatus.cur_user != '':
314 vstatus.time_to_idle = None
317 ## FIXME - this may need to be elsewhere.....
319 vstatus.mk.update_display()
323 def handle_get_selection_key(state, event, params, v, vstatus):
325 if len(vstatus.cur_selection) == 0:
328 vstatus.cur_user = ''
329 vstatus.cur_selection = ''
332 vstatus.state = STATE_IDLE
334 vstatus.mk.set_messages(
335 [(center('BYE!'), False, 1.5),
336 (GREETING, False, None)])
338 vstatus.cur_selection += chr(key + ord('0'))
339 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
340 vstatus.time_to_autologout = None
341 elif len(vstatus.cur_selection) == 1:
343 vstatus.cur_selection = ''
344 vstatus.time_to_autologout = None
345 scroll_options(vstatus.username, vstatus.mk)
348 vstatus.cur_selection += chr(key + ord('0'))
349 make_selection(v,vstatus)
350 vstatus.cur_selection = ''
351 vstatus.time_to_autologout = time() + 8
352 vstatus.last_timeout_refresh = None
354 def make_selection(v, vstatus):
355 # should use sudo here
356 if vstatus.cur_selection == '55':
357 vstatus.mk.set_message('OPENSESAME')
358 logging.info('dispensing a door for %s'%vstatus.username)
360 ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
362 ret = os.system('dispense door')
364 logging.info('door opened')
365 vstatus.mk.set_message(center('DOOR OPEN'))
367 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
368 vstatus.mk.set_message(center('BAD DOOR'))
370 elif vstatus.cur_selection == '91':
372 elif vstatus.cur_selection == '99':
373 scroll_options(vstatus.username, vstatus.mk)
374 vstatus.cur_selection = ''
376 elif vstatus.cur_selection[1] == '8':
377 v.display('GOT COKE?')
378 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
379 v.display('SEEMS NOT')
381 v.display('GOT COKE!')
383 v.display(vstatus.cur_selection+' - $1.00')
384 if ((os.system('su - "%s" -c "dispense snack"'%(vstatus.username)) >> 8) == 0):
385 v.vend(vstatus.cur_selection)
386 v.display('THANK YOU')
388 v.display('NO MONEY?')
392 def handle_getting_pin_key(state, event, params, v, vstatus):
393 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
395 if len(vstatus.cur_pin) < PIN_LENGTH:
397 if vstatus.cur_pin == '':
398 vstatus.cur_user = ''
399 vstatus.mk.set_message(GREETING)
402 vstatus.state = STATE_IDLE
406 vstatus.mk.set_message('PIN: ')
408 vstatus.cur_pin += chr(key + ord('0'))
409 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
410 if len(vstatus.cur_pin) == PIN_LENGTH:
411 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
414 vstatus.cur_selection = ''
415 vstatus.state = STATE_GET_SELECTION
416 scroll_options(vstatus.username, vstatus.mk, True)
420 vstatus.mk.set_messages(
421 [(center('BAD PIN'), False, 1.0),
422 (center('SORRY'), False, 0.5),
423 (GREETING, False, None)])
424 vstatus.cur_user = ''
428 vstatus.state = STATE_IDLE
433 def handle_getting_uid_key(state, event, params, v, vstatus):
434 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
436 # complicated key handling here:
437 if len(vstatus.cur_user) < 5:
439 vstatus.cur_user = ''
440 vstatus.mk.set_message(GREETING)
443 vstatus.state = STATE_IDLE
446 vstatus.cur_user += chr(key + ord('0'))
447 vstatus.mk.set_message('UID: '+vstatus.cur_user)
449 if len(vstatus.cur_user) == 5:
450 uid = int(vstatus.cur_user)
451 if not has_good_pin(uid):
452 logging.info('user '+vstatus.cur_user+' has a bad PIN')
453 vstatus.mk.set_messages(
454 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
455 (GREETING, False, None)])
456 vstatus.cur_user = ''
460 vstatus.state = STATE_IDLE
466 vstatus.mk.set_message('PIN: ')
467 logging.info('need pin for user %s'%vstatus.cur_user)
468 vstatus.state = STATE_GETTING_PIN
472 def handle_idle_key(state, event, params, v, vstatus):
473 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
478 vstatus.cur_user = ''
479 vstatus.mk.set_message(GREETING)
484 vstatus.state = STATE_GETTING_UID
485 run_handler(event, key, v, vstatus)
488 def handle_idle_tick(state, event, params, v, vstatus):
489 ### State logged out ... initiate idler in 5 (first start?)
490 if vstatus.time_to_idle == None and vstatus.cur_user == '':
491 vstatus.time_to_idle = time() + 5
496 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle:
499 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle + 30:
500 vstatus.time_to_idle = time()
505 vstatus.mk.update_display()
508 def handle_door_idle(state, event, params, v, vstatus):
509 # don't care right now.
512 def handle_door_event(state, event, params, v, vstatus):
513 vstatus.time_to_idle = None
515 if params == 1: #door open
516 vstatus.state = STATE_DOOR_OPENING
517 logging.warning("Entering open door mode")
518 v.display("-FEED ME-")
520 vstatus.cur_user = ''
522 elif params == 0: #door closed
523 vstatus.state = STATE_DOOR_CLOSING
526 logging.warning('Leaving open door mode')
527 v.display("-YUM YUM!-")
529 def idle_in(vstatus,seconds):
530 vstatus.time_to_idle = time() + seconds
532 def return_to_idle(state,event,params,v,vstatus):
533 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle:
534 vstatus.mk.set_message(GREETING)
535 vstatus.state = STATE_IDLE
537 if not vstatus.time_to_idle:
538 vstatus.mk.set_message(GREETING)
539 vstatus.state = STATE_IDLE
542 def create_state_table(vstatus):
543 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
544 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
545 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
547 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
548 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
549 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
551 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
552 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
553 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
555 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
556 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
557 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
559 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
560 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
561 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
563 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
564 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
565 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
567 def get_state_table_handler(vstatus, state, event, counter):
568 return vstatus.state_table[(state,event,counter)]
570 def run_forever(rfh, wfh, options, cf):
571 v = VendingMachine(rfh, wfh)
572 vstatus = VendState(v)
573 create_state_table(vstatus)
575 logging.debug('PING is ' + str(v.ping()))
577 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
579 vstatus.mk.set_message(GREETING)
582 vstatus.mk.set_message("Booted")
585 # This main loop was hideous and the work of the devil.
586 # This has now been fixed (mostly) - mtearle
589 # notes for later surgery
590 # (event, counter, ' ')
594 # ( return state - not currently implemented )
600 except DispenseDatabaseException, e:
601 logging.error('Database error: '+str(e))
608 run_handler(event, params, v, vstatus)
610 # logging.debug('Got event: ' + repr(e))
613 def run_handler(event, params, v, vstatus):
614 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
616 handler(vstatus.state, event, params, v, vstatus)
618 def connect_to_vend(options, cf):
621 logging.info('Connecting to vending machine using LAT')
622 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
623 rfh, wfh = latclient.get_fh()
624 elif options.use_serial:
625 # Open vending machine via serial.
626 logging.info('Connecting to vending machine using serial')
627 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
628 rfh,wfh = serialclient.get_fh()
630 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
631 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
633 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
634 sock.connect((options.host, options.port))
635 rfh = sock.makefile('r')
636 wfh = sock.makefile('w')
641 from optparse import OptionParser
643 op = OptionParser(usage="%prog [OPTION]...")
644 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')
645 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
646 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
647 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
648 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
649 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
650 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
651 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
652 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
653 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
654 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
655 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
656 options, args = op.parse_args()
659 op.error('extra command line arguments: ' + ' '.join(args))
664 'DBServer': ('Database', 'Server'),
665 'DBName': ('Database', 'Name'),
666 'DBUser': ('VendingMachine', 'DBUser'),
667 'DBPassword': ('VendingMachine', 'DBPassword'),
669 'ServiceName': ('VendingMachine', 'ServiceName'),
670 'ServicePassword': ('VendingMachine', 'Password'),
672 'ServerName': ('DecServer', 'Name'),
673 'ConnectPassword': ('DecServer', 'ConnectPassword'),
674 'PrivPassword': ('DecServer', 'PrivPassword'),
677 class VendConfigFile:
678 def __init__(self, config_file, options):
680 cp = ConfigParser.ConfigParser()
683 for option in options:
684 section, name = options[option]
685 value = cp.get(section, name)
686 self.__dict__[option] = value
688 except ConfigParser.Error, e:
689 raise SystemExit("Error reading config file "+config_file+": " + str(e))
691 def create_pid_file(name):
693 pid_file = file(name, 'w')
694 pid_file.write('%d\n'%os.getpid())
697 logging.warning('unable to write to pid file '+name+': '+str(e))
700 def do_nothing(signum, stack):
701 signal.signal(signum, do_nothing)
702 def stop_server(signum, stack): raise KeyboardInterrupt
703 signal.signal(signal.SIGHUP, do_nothing)
704 signal.signal(signal.SIGTERM, stop_server)
705 signal.signal(signal.SIGINT, stop_server)
707 options = parse_args()
708 config_opts = VendConfigFile(options.config_file, config_options)
709 if options.daemon: become_daemon()
710 set_up_logging(options)
711 if options.pid_file != '': create_pid_file(options.pid_file)
713 return options, config_opts
715 def clean_up_nicely(options, config_opts):
716 if options.pid_file != '':
718 os.unlink(options.pid_file)
719 logging.debug('Removed pid file '+options.pid_file)
720 except OSError: pass # if we can't delete it, meh
722 def set_up_logging(options):
723 logger = logging.getLogger()
725 if not options.daemon:
726 stderr_logger = logging.StreamHandler(sys.stderr)
727 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
728 logger.addHandler(stderr_logger)
730 if options.log_file != '':
732 file_logger = logging.FileHandler(options.log_file)
733 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
734 logger.addHandler(file_logger)
736 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
738 if options.syslog != None:
739 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
740 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
741 logger.addHandler(sys_logger)
744 logger.setLevel(logging.WARNING)
745 elif options.verbose:
746 logger.setLevel(logging.DEBUG)
748 logger.setLevel(logging.INFO)
751 dev_null = file('/dev/null')
752 fd = dev_null.fileno()
761 raise SystemExit('failed to fork: '+str(e))
763 def do_vend_server(options, config_opts):
766 rfh, wfh = connect_to_vend(options, config_opts)
767 except (SerialClientException, socket.error), e:
768 (exc_type, exc_value, exc_traceback) = sys.exc_info()
770 logging.error("Connection error: "+str(exc_type)+" "+str(e))
771 logging.info("Trying again in 5 seconds.")
776 run_forever(rfh, wfh, options, config_opts)
777 except VendingException:
778 logging.error("Connection died, trying again...")
779 logging.info("Trying again in 5 seconds.")
782 if __name__ == '__main__':
783 options, config_opts = set_stuff_up()
786 logging.warning('Starting Vend Server')
787 do_vend_server(options, config_opts)
788 logging.error('Vend Server finished unexpectedly, restarting')
789 except KeyboardInterrupt:
790 logging.info("Killed by signal, cleaning up")
791 clean_up_nicely(options, config_opts)
792 logging.warning("Vend Server stopped")
797 (exc_type, exc_value, exc_traceback) = sys.exc_info()
798 tb = format_tb(exc_traceback, 20)
801 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
802 logging.critical("Message: " + str(exc_value))
803 logging.critical("Traceback:")
805 for line in event.split('\n'):
806 logging.critical(' '+line)
807 logging.critical("This message should be considered a bug in the Vend Server.")
808 logging.critical("Please report this to someone who can fix it.")
810 logging.warning("Trying again anyway (might not help, but hey...)")