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'
47 class DispenseDatabaseException(Exception): pass
49 class DispenseDatabase:
50 def __init__(self, vending_machine, host, name, user, password):
51 self.vending_machine = vending_machine
52 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
53 self.db.query('LISTEN vend_requests')
55 def process_requests(self):
56 logging.debug('database processing')
57 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
59 outstanding = self.db.query(query).getresult()
60 except (pg.error,), db_err:
61 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
62 for (id, slot) in outstanding:
63 (worked, code, string) = self.vending_machine.vend(slot)
64 logging.debug (str((worked, code, string)))
66 query = 'SELECT vend_success(%s)'%id
67 self.db.query(query).getresult()
69 query = 'SELECT vend_failed(%s)'%id
70 self.db.query(query).getresult()
72 def handle_events(self):
73 notifier = self.db.getnotify()
74 while notifier is not None:
75 self.process_requests()
76 notify = self.db.getnotify()
78 def scroll_options(username, mk, welcome = False):
80 msg = [(center('WELCOME'), False, 0.8),
81 (center(username), False, 0.8)]
84 choices = ' '*10+'CHOICES: '
86 coke_machine = file('/home/other/coke/coke_contents')
87 cokes = coke_machine.readlines()
94 (slot_num, price, slot_name) = c.split(' ', 2)
95 if slot_name == 'dead': continue
96 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
98 choices += 'OR A SNACK. '
99 choices += '99 TO READ AGAIN. '
100 choices += 'CHOICE? '
101 msg.append((choices, False, None))
106 info = pwd.getpwuid(uid)
108 logging.info('getting pin for uid %d: user not in password file'%uid)
110 if info.pw_dir == None: return False
111 pinfile = os.path.join(info.pw_dir, '.pin')
115 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
118 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
123 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
125 pinstr = f.readline()
127 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
128 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
132 def has_good_pin(uid):
133 return get_pin(uid) != None
135 def verify_user_pin(uid, pin):
136 if get_pin(uid) == pin:
137 info = pwd.getpwuid(uid)
138 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
141 logging.info('refused pin for uid %d'%(uid))
144 def door_open_mode(v):
145 logging.warning("Entering open door mode")
146 v.display("-FEED ME-")
151 if event == TICK: break
154 if params == 1: # door closed
155 logging.warning('Leaving open door mode')
156 v.display("-YUM YUM!-")
162 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
163 choice = int(random()*len(messages))
164 msg = messages[choice]
165 left = range(len(msg))
166 for i in range(len(msg)):
167 if msg[i] == ' ': left.remove(i)
171 for i in range(0, len(msg)):
177 s += chr(int(random()*26)+ord('A'))
186 return ' '*((LEN-len(str))/2)+str
197 StringIdler(v, text="Kill 'em all", repeat=False),
198 GrayIdler(v,one="*",zero="-"),
199 StringIdler(v, text=CREDITS),
200 GrayIdler(v,one="/",zero="\\"),
202 GrayIdler(v,one="X",zero="O"),
203 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
204 GrayIdler(v,one="*",zero="-",reorder=1),
205 StringIdler(v, text=str(math.pi) + " "),
207 GrayIdler(v,one="/",zero="\\",reorder=1),
208 StringIdler(v, text=str(math.e) + " "),
209 GrayIdler(v,one="X",zero="O",reorder=1),
210 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),
211 PipeIdler(v, "/usr/bin/ypcat", "passwd"),
219 idler = choose_idler()
226 iiindex = idlers.index(idler)
230 move = int(random()*len(idlers)) + 1
233 idler = idlers[( (iiindex + 1) % iilen)]
234 move = move - idler.affinity()
245 def __init__(self,v):
246 self.mk = MessageKeeper(v)
249 self.cur_selection = ''
250 self.time_to_autologout = None
251 self.time_to_idle = None
252 self.last_timeout_refresh = None
255 def handle_door_event(event, params, v, vstatus):
258 vstatus.cur_user = ''
260 vstatus.mk.set_message(GREETING)
262 def handle_tick_event(event, params, v, vstatus):
263 # don't care right now.
266 def handle_switch_event(event, params, v, vstatus):
267 # don't care right now.
270 def handle_key_event(event, params, v, vstatus):
272 # complicated key handling here:
273 if len(vstatus.cur_user) < 5:
275 vstatus.cur_user = ''
276 vstatus.mk.set_message(GREETING)
278 vstatus.cur_user += chr(key + ord('0'))
279 vstatus.mk.set_message('UID: '+vstatus.cur_user)
280 if len(vstatus.cur_user) == 5:
281 uid = int(vstatus.cur_user)
282 if not has_good_pin(uid):
283 logging.info('user '+vstatus.cur_user+' has a bad PIN')
285 #[(center('INVALID'), False, 0.7),
286 #(center('PIN'), False, 0.7),
287 #(center('SETUP'), False, 1.0),
288 #(GREETING, False, None)])
289 vstatus.mk.set_messages(
290 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
291 (GREETING, False, None)])
292 vstatus.cur_user = ''
296 vstatus.mk.set_message('PIN: ')
297 logging.info('need pin for user %s'%vstatus.cur_user)
299 elif len(vstatus.cur_pin) < PIN_LENGTH:
301 if vstatus.cur_pin == '':
302 vstatus.cur_user = ''
303 vstatus.mk.set_message(GREETING)
306 vstatus.mk.set_message('PIN: ')
308 vstatus.cur_pin += chr(key + ord('0'))
309 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
310 if len(vstatus.cur_pin) == PIN_LENGTH:
311 username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
314 vstatus.cur_selection = ''
315 scroll_options(username, vstatus.mk, True)
319 vstatus.mk.set_messages(
320 [(center('BAD PIN'), False, 1.0),
321 (center('SORRY'), False, 0.5),
322 (GREETING, False, None)])
323 vstatus.cur_user = ''
326 elif len(vstatus.cur_selection) == 0:
329 vstatus.cur_user = ''
330 vstatus.cur_selection = ''
331 vstatus.mk.set_messages(
332 [(center('BYE!'), False, 1.5),
333 (GREETING, False, None)])
335 vstatus.cur_selection += chr(key + ord('0'))
336 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
337 vstatus.time_to_autologout = None
338 elif len(vstatus.cur_selection) == 1:
340 vstatus.cur_selection = ''
341 vstatus.time_to_autologout = None
342 scroll_options(username, vstatus.mk)
345 vstatus.cur_selection += chr(key + ord('0'))
346 #make_selection(cur_selection)
347 # XXX this should move somewhere else:
348 if vstatus.cur_selection == '55':
349 vstatus.mk.set_message('OPENSESAME')
350 logging.info('dispensing a door for %s'%username)
352 ret = os.system('su - "%s" -c "dispense door"'%username)
354 ret = os.system('dispense door')
356 logging.info('door opened')
357 vstatus.mk.set_message(center('DOOR OPEN'))
359 logging.warning('user %s tried to dispense a bad door'%username)
360 vstatus.mk.set_message(center('BAD DOOR'))
362 elif vstatus.cur_selection == '91':
364 elif vstatus.cur_selection == '99':
365 scroll_options(username, vstatus.mk)
366 vstatus.cur_selection = ''
368 elif vstatus.cur_selection[1] == '8':
369 v.display('GOT COKE?')
370 if ((os.system('su - "%s" -c "dispense %s"'%(username, vstatus.cur_selection[0])) >> 8) != 0):
371 v.display('SEEMS NOT')
373 v.display('GOT COKE!')
375 v.display(vstatus.cur_selection+' - $1.00')
376 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
377 v.vend(vstatus.cur_selection)
378 v.display('THANK YOU')
380 v.display('NO MONEY?')
382 vstatus.cur_selection = ''
383 vstatus.time_to_autologout = time() + 8
384 vstatus.last_timeout_refresh = None
387 def run_forever(rfh, wfh, options, cf):
388 v = VendingMachine(rfh, wfh)
389 vstatus = VendState(v)
391 logging.debug('PING is ' + str(v.ping()))
393 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
395 vstatus.mk.set_message(GREETING)
399 # This main loop is hideous and the work of the devil - mtearle
402 # notes for later surgery
403 # (event, counter, ' ')
413 except DispenseDatabaseException, e:
414 logging.error('Database error: '+str(e))
416 if vstatus.time_to_autologout != None:
417 time_left = vstatus.time_to_autologout - time()
418 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
419 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
420 vstatus.last_timeout_refresh = int(time_left)
421 vstatus.cur_selection = ''
423 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
424 vstatus.time_to_autologout = None
425 vstatus.cur_user = ''
427 vstatus.cur_selection = ''
428 vstatus.mk.set_message(GREETING)
430 if vstatus.time_to_autologout and not vstatus.mk.done():
431 vstatus.time_to_autologout = None
432 if vstatus.cur_user == '' and vstatus.time_to_autologout:
433 vstatus.time_to_autologout = None
434 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
436 vstatus.time_to_autologout = time() + 15
437 vstatus.last_timeout_refresh = None
439 if vstatus.time_to_idle == None and vstatus.cur_user == '':
440 vstatus.time_to_idle = time() + 5
442 if vstatus.time_to_idle is not None and vstatus.cur_user != '':
443 vstatus.time_to_idle = None
445 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle:
448 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle + 30:
449 vstatus.time_to_idle = time()
452 vstatus.mk.update_display()
458 e = v.next_event(0.05)
462 handle_tick_event(event, params, v, vstatus)
464 vstatus.time_to_idle = None
465 logging.debug('Got event: ' + repr(e))
468 handle_door_event(event, params, v, vstatus)
469 elif event == SWITCH:
470 handle_switch_event(event, params, v, vstatus)
472 handle_key_event(event, params, v, vstatus)
474 def connect_to_vend(options, cf):
477 logging.info('Connecting to vending machine using LAT')
478 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
479 rfh, wfh = latclient.get_fh()
480 elif options.use_serial:
481 # Open vending machine via serial.
482 logging.info('Connecting to vending machine using serial')
483 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
484 rfh,wfh = serialclient.get_fh()
486 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
487 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
489 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
490 sock.connect((options.host, options.port))
491 rfh = sock.makefile('r')
492 wfh = sock.makefile('w')
497 from optparse import OptionParser
499 op = OptionParser(usage="%prog [OPTION]...")
500 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')
501 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
502 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
503 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
504 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
505 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
506 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
507 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
508 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
509 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
510 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
511 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
512 options, args = op.parse_args()
515 op.error('extra command line arguments: ' + ' '.join(args))
520 'DBServer': ('Database', 'Server'),
521 'DBName': ('Database', 'Name'),
522 'DBUser': ('VendingMachine', 'DBUser'),
523 'DBPassword': ('VendingMachine', 'DBPassword'),
525 'ServiceName': ('VendingMachine', 'ServiceName'),
526 'ServicePassword': ('VendingMachine', 'Password'),
528 'ServerName': ('DecServer', 'Name'),
529 'ConnectPassword': ('DecServer', 'ConnectPassword'),
530 'PrivPassword': ('DecServer', 'PrivPassword'),
533 class VendConfigFile:
534 def __init__(self, config_file, options):
536 cp = ConfigParser.ConfigParser()
539 for option in options:
540 section, name = options[option]
541 value = cp.get(section, name)
542 self.__dict__[option] = value
544 except ConfigParser.Error, e:
545 raise SystemExit("Error reading config file "+config_file+": " + str(e))
547 def create_pid_file(name):
549 pid_file = file(name, 'w')
550 pid_file.write('%d\n'%os.getpid())
553 logging.warning('unable to write to pid file '+name+': '+str(e))
556 def do_nothing(signum, stack):
557 signal.signal(signum, do_nothing)
558 def stop_server(signum, stack): raise KeyboardInterrupt
559 signal.signal(signal.SIGHUP, do_nothing)
560 signal.signal(signal.SIGTERM, stop_server)
561 signal.signal(signal.SIGINT, stop_server)
563 options = parse_args()
564 config_opts = VendConfigFile(options.config_file, config_options)
565 if options.daemon: become_daemon()
566 set_up_logging(options)
567 if options.pid_file != '': create_pid_file(options.pid_file)
569 return options, config_opts
571 def clean_up_nicely(options, config_opts):
572 if options.pid_file != '':
574 os.unlink(options.pid_file)
575 logging.debug('Removed pid file '+options.pid_file)
576 except OSError: pass # if we can't delete it, meh
578 def set_up_logging(options):
579 logger = logging.getLogger()
581 if not options.daemon:
582 stderr_logger = logging.StreamHandler(sys.stderr)
583 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
584 logger.addHandler(stderr_logger)
586 if options.log_file != '':
588 file_logger = logging.FileHandler(options.log_file)
589 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
590 logger.addHandler(file_logger)
592 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
594 if options.syslog != None:
595 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
596 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
597 logger.addHandler(sys_logger)
600 logger.setLevel(logging.WARNING)
601 elif options.verbose:
602 logger.setLevel(logging.DEBUG)
604 logger.setLevel(logging.INFO)
607 dev_null = file('/dev/null')
608 fd = dev_null.fileno()
617 raise SystemExit('failed to fork: '+str(e))
619 def do_vend_server(options, config_opts):
622 rfh, wfh = connect_to_vend(options, config_opts)
623 except (SerialClientException, socket.error), e:
624 (exc_type, exc_value, exc_traceback) = sys.exc_info()
626 logging.error("Connection error: "+str(exc_type)+" "+str(e))
627 logging.info("Trying again in 5 seconds.")
632 run_forever(rfh, wfh, options, config_opts)
633 except VendingException:
634 logging.error("Connection died, trying again...")
635 logging.info("Trying again in 5 seconds.")
638 if __name__ == '__main__':
639 options, config_opts = set_stuff_up()
642 logging.warning('Starting Vend Server')
643 do_vend_server(options, config_opts)
644 logging.error('Vend Server finished unexpectedly, restarting')
645 except KeyboardInterrupt:
646 logging.info("Killed by signal, cleaning up")
647 clean_up_nicely(options, config_opts)
648 logging.warning("Vend Server stopped")
653 (exc_type, exc_value, exc_traceback) = sys.exc_info()
654 tb = format_tb(exc_traceback, 20)
657 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
658 logging.critical("Message: " + str(exc_value))
659 logging.critical("Traceback:")
661 for line in event.split('\n'):
662 logging.critical(' '+line)
663 logging.critical("This message should be considered a bug in the Vend Server.")
664 logging.critical("Please report this to someone who can fix it.")
666 logging.warning("Trying again anyway (might not help, but hey...)")