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'
46 class DispenseDatabaseException(Exception): pass
48 class DispenseDatabase:
49 def __init__(self, vending_machine, host, name, user, password):
50 self.vending_machine = vending_machine
51 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
52 self.db.query('LISTEN vend_requests')
54 def process_requests(self):
55 logging.debug('database processing')
56 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
58 outstanding = self.db.query(query).getresult()
59 except (pg.error,), db_err:
60 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
61 for (id, slot) in outstanding:
62 (worked, code, string) = self.vending_machine.vend(slot)
63 logging.debug (str((worked, code, string)))
65 query = 'SELECT vend_success(%s)'%id
66 self.db.query(query).getresult()
68 query = 'SELECT vend_failed(%s)'%id
69 self.db.query(query).getresult()
71 def handle_events(self):
72 notifier = self.db.getnotify()
73 while notifier is not None:
74 self.process_requests()
75 notify = self.db.getnotify()
77 def scroll_options(username, mk, welcome = False):
79 msg = [(center('WELCOME'), False, 0.8),
80 (center(username), False, 0.8)]
83 choices = ' '*10+'CHOICES: '
85 coke_machine = file('/home/other/coke/coke_contents')
86 cokes = coke_machine.readlines()
93 (slot_num, price, slot_name) = c.split(' ', 2)
94 if slot_name == 'dead': continue
95 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
97 choices += 'OR A SNACK. '
98 choices += '99 TO READ AGAIN. '
100 msg.append((choices, False, None))
105 info = pwd.getpwuid(uid)
107 logging.info('getting pin for uid %d: user not in password file'%uid)
109 if info.pw_dir == None: return False
110 pinfile = os.path.join(info.pw_dir, '.pin')
114 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
117 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
122 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
124 pinstr = f.readline()
126 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
127 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
131 def has_good_pin(uid):
132 return get_pin(uid) != None
134 def verify_user_pin(uid, pin):
135 if get_pin(uid) == pin:
136 info = pwd.getpwuid(uid)
137 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
140 logging.info('refused pin for uid %d'%(uid))
143 def door_open_mode(v):
144 logging.warning("Entering open door mode")
145 v.display("-FEED ME-")
151 if params == 1: # door closed
152 logging.warning('Leaving open door mode')
153 v.display("-YUM YUM!-")
159 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
160 choice = int(random()*len(messages))
161 msg = messages[choice]
162 left = range(len(msg))
163 for i in range(len(msg)):
164 if msg[i] == ' ': left.remove(i)
168 for i in range(0, len(msg)):
174 s += chr(int(random()*26)+ord('A'))
183 return ' '*((LEN-len(str))/2)+str
194 StringIdler(v, text="Kill 'em all", repeat=False),
195 GrayIdler(v,one="*",zero="-"),
196 StringIdler(v, text=CREDITS),
197 GrayIdler(v,one="/",zero="\\"),
199 GrayIdler(v,one="X",zero="O"),
200 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
201 GrayIdler(v,one="*",zero="-",reorder=1),
202 StringIdler(v, text=str(math.pi) + " "),
204 GrayIdler(v,one="/",zero="\\",reorder=1),
205 StringIdler(v, text=str(math.e) + " "),
206 GrayIdler(v,one="X",zero="O",reorder=1),
207 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),
208 PipeIdler(v, "/usr/bin/ypcat", "passwd"),
216 idler = choose_idler()
223 iiindex = idlers.index(idler)
227 move = int(random()*len(idlers)) + 1
230 idler = idlers[( (iiindex + 1) % iilen)]
231 move = move - idler.affinity()
241 def run_forever(rfh, wfh, options, cf):
242 v = VendingMachine(rfh, wfh)
243 logging.debug('PING is ' + str(v.ping()))
245 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
250 mk = MessageKeeper(v)
251 mk.set_message(GREETING)
252 time_to_autologout = None
255 last_timeout_refresh = None
258 # This main loop is hideous and the work of the devil - mtearle
261 # notes for later surgery
262 # (event, counter, ' ')
272 except DispenseDatabaseException, e:
273 logging.error('Database error: '+str(e))
275 if time_to_autologout != None:
276 time_left = time_to_autologout - time()
277 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
278 mk.set_message('LOGOUT: '+str(int(time_left)))
279 last_timeout_refresh = int(time_left)
282 if time_to_autologout != None and time_to_autologout - time() <= 0:
283 time_to_autologout = None
287 mk.set_message(GREETING)
289 if time_to_autologout and not mk.done(): time_to_autologout = None
290 if cur_user == '' and time_to_autologout: time_to_autologout = None
291 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
293 time_to_autologout = time() + 15
294 last_timeout_refresh = None
296 if time_to_idle == None and cur_user == '':
297 time_to_idle = time() + 5
299 if time_to_idle is not None and cur_user != '': time_to_idle = None
300 if time_to_idle is not None and time() > time_to_idle: idle_step()
301 if time_to_idle is not None and time() > time_to_idle + 300:
302 time_to_idle = time()
309 e = v.next_event(0.05)
314 logging.debug('Got event: ' + repr(e))
320 mk.set_message(GREETING)
321 elif event == SWITCH:
322 # don't care right now.
326 # complicated key handling here:
327 if len(cur_user) < 5:
330 mk.set_message(GREETING)
332 cur_user += chr(key + ord('0'))
333 mk.set_message('UID: '+cur_user)
334 if len(cur_user) == 5:
336 if not has_good_pin(uid):
337 logging.info('user '+cur_user+' has a bad PIN')
339 #[(center('INVALID'), False, 0.7),
340 #(center('PIN'), False, 0.7),
341 #(center('SETUP'), False, 1.0),
342 #(GREETING, False, None)])
344 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
345 (GREETING, False, None)])
350 mk.set_message('PIN: ')
351 logging.info('need pin for user %s'%cur_user)
353 elif len(cur_pin) < PIN_LENGTH:
357 mk.set_message(GREETING)
360 mk.set_message('PIN: ')
362 cur_pin += chr(key + ord('0'))
363 mk.set_message('PIN: '+'X'*len(cur_pin))
364 if len(cur_pin) == PIN_LENGTH:
365 username = verify_user_pin(int(cur_user), int(cur_pin))
369 scroll_options(username, mk, True)
374 [(center('BAD PIN'), False, 1.0),
375 (center('SORRY'), False, 0.5),
376 (GREETING, False, None)])
380 elif len(cur_selection) == 0:
386 [(center('BYE!'), False, 1.5),
387 (GREETING, False, None)])
389 cur_selection += chr(key + ord('0'))
390 mk.set_message('SELECT: '+cur_selection)
391 time_to_autologout = None
392 elif len(cur_selection) == 1:
395 time_to_autologout = None
396 scroll_options(username, mk)
399 cur_selection += chr(key + ord('0'))
400 #make_selection(cur_selection)
401 # XXX this should move somewhere else:
402 if cur_selection == '55':
403 mk.set_message('OPENSESAME')
404 logging.info('dispensing a door for %s'%username)
406 ret = os.system('su - "%s" -c "dispense door"'%username)
408 ret = os.system('dispense door')
410 logging.info('door opened')
411 mk.set_message(center('DOOR OPEN'))
413 logging.warning('user %s tried to dispense a bad door'%username)
414 mk.set_message(center('BAD DOOR'))
416 elif cur_selection == '91':
418 elif cur_selection == '99':
419 scroll_options(username, mk)
422 elif cur_selection[1] == '8':
423 v.display('GOT COKE?')
424 if ((os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0])) >> 8) != 0):
425 v.display('SEEMS NOT')
427 v.display('GOT COKE!')
429 v.display(cur_selection+' - $1.00')
430 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
431 v.vend(cur_selection)
432 v.display('THANK YOU')
434 v.display('NO MONEY?')
437 time_to_autologout = time() + 8
438 last_timeout_refresh = None
440 def connect_to_vend(options, cf):
443 logging.info('Connecting to vending machine using LAT')
444 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
445 rfh, wfh = latclient.get_fh()
446 elif options.use_serial:
447 # Open vending machine via serial.
448 logging.info('Connecting to vending machine using serial')
449 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
450 rfh,wfh = serialclient.get_fh()
452 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
453 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
455 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
456 sock.connect((options.host, options.port))
457 rfh = sock.makefile('r')
458 wfh = sock.makefile('w')
463 from optparse import OptionParser
465 op = OptionParser(usage="%prog [OPTION]...")
466 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')
467 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
468 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
469 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
470 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
471 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
472 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
473 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
474 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
475 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
476 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
477 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
478 options, args = op.parse_args()
481 op.error('extra command line arguments: ' + ' '.join(args))
486 'DBServer': ('Database', 'Server'),
487 'DBName': ('Database', 'Name'),
488 'DBUser': ('VendingMachine', 'DBUser'),
489 'DBPassword': ('VendingMachine', 'DBPassword'),
491 'ServiceName': ('VendingMachine', 'ServiceName'),
492 'ServicePassword': ('VendingMachine', 'Password'),
494 'ServerName': ('DecServer', 'Name'),
495 'ConnectPassword': ('DecServer', 'ConnectPassword'),
496 'PrivPassword': ('DecServer', 'PrivPassword'),
499 class VendConfigFile:
500 def __init__(self, config_file, options):
502 cp = ConfigParser.ConfigParser()
505 for option in options:
506 section, name = options[option]
507 value = cp.get(section, name)
508 self.__dict__[option] = value
510 except ConfigParser.Error, e:
511 raise SystemExit("Error reading config file "+config_file+": " + str(e))
513 def create_pid_file(name):
515 pid_file = file(name, 'w')
516 pid_file.write('%d\n'%os.getpid())
519 logging.warning('unable to write to pid file '+name+': '+str(e))
522 def do_nothing(signum, stack):
523 signal.signal(signum, do_nothing)
524 def stop_server(signum, stack): raise KeyboardInterrupt
525 signal.signal(signal.SIGHUP, do_nothing)
526 signal.signal(signal.SIGTERM, stop_server)
527 signal.signal(signal.SIGINT, stop_server)
529 options = parse_args()
530 config_opts = VendConfigFile(options.config_file, config_options)
531 if options.daemon: become_daemon()
532 set_up_logging(options)
533 if options.pid_file != '': create_pid_file(options.pid_file)
535 return options, config_opts
537 def clean_up_nicely(options, config_opts):
538 if options.pid_file != '':
540 os.unlink(options.pid_file)
541 logging.debug('Removed pid file '+options.pid_file)
542 except OSError: pass # if we can't delete it, meh
544 def set_up_logging(options):
545 logger = logging.getLogger()
547 if not options.daemon:
548 stderr_logger = logging.StreamHandler(sys.stderr)
549 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
550 logger.addHandler(stderr_logger)
552 if options.log_file != '':
554 file_logger = logging.FileHandler(options.log_file)
555 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
556 logger.addHandler(file_logger)
558 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
560 if options.syslog != None:
561 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
562 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
563 logger.addHandler(sys_logger)
566 logger.setLevel(logging.WARNING)
567 elif options.verbose:
568 logger.setLevel(logging.DEBUG)
570 logger.setLevel(logging.INFO)
573 dev_null = file('/dev/null')
574 fd = dev_null.fileno()
583 raise SystemExit('failed to fork: '+str(e))
585 def do_vend_server(options, config_opts):
588 rfh, wfh = connect_to_vend(options, config_opts)
589 except (SerialClientException, socket.error), e:
590 (exc_type, exc_value, exc_traceback) = sys.exc_info()
592 logging.error("Connection error: "+str(exc_type)+" "+str(e))
593 logging.info("Trying again in 5 seconds.")
598 run_forever(rfh, wfh, options, config_opts)
599 except VendingException:
600 logging.error("Connection died, trying again...")
601 logging.info("Trying again in 5 seconds.")
604 if __name__ == '__main__':
605 options, config_opts = set_stuff_up()
608 logging.warning('Starting Vend Server')
609 do_vend_server(options, config_opts)
610 logging.error('Vend Server finished unexpectedly, restarting')
611 except KeyboardInterrupt:
612 logging.info("Killed by signal, cleaning up")
613 clean_up_nicely(options, config_opts)
614 logging.warning("Vend Server stopped")
619 (exc_type, exc_value, exc_traceback) = sys.exc_info()
620 tb = format_tb(exc_traceback, 20)
623 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
624 logging.critical("Message: " + str(exc_value))
625 logging.critical("Traceback:")
627 for line in event.split('\n'):
628 logging.critical(' '+line)
629 logging.critical("This message should be considered a bug in the Vend Server.")
630 logging.critical("Please report this to someone who can fix it.")
632 logging.warning("Trying again anyway (might not help, but hey...)")