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 VendingMachine import VendingMachine, VendingException
15 from MessageKeeper import MessageKeeper
16 from HorizScroll import HorizScroll
17 from random import random, seed
18 from Idler import TrainIdler,GrayIdler,StringIdler
20 from posix import geteuid
23 This vending machine software brought to you by:
28 and a collective of hungry alpacas.
32 For a good time call +61 8 6488 3901
38 GREETING = 'UCC SNACKS'
45 class DispenseDatabaseException(Exception): pass
47 class DispenseDatabase:
48 def __init__(self, vending_machine, host, name, user, password):
49 self.vending_machine = vending_machine
50 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
51 self.db.query('LISTEN vend_requests')
53 def process_requests(self):
54 logging.debug('database processing')
55 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
57 outstanding = self.db.query(query).getresult()
58 except (pg.error,), db_err:
59 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
60 for (id, slot) in outstanding:
61 (worked, code, string) = self.vending_machine.vend(slot)
62 logging.debug (str((worked, code, string)))
64 query = 'SELECT vend_success(%s)'%id
65 self.db.query(query).getresult()
67 query = 'SELECT vend_failed(%s)'%id
68 self.db.query(query).getresult()
70 def handle_events(self):
71 notifier = self.db.getnotify()
72 while notifier is not None:
73 self.process_requests()
74 notify = self.db.getnotify()
76 def scroll_options(username, mk, welcome = False):
78 msg = [(center('WELCOME'), False, 0.8),
79 (center(username), False, 0.8)]
82 choices = ' '*10+'CHOICES: '
84 coke_machine = file('/home/other/coke/coke_contents')
85 cokes = coke_machine.readlines()
92 (slot_num, price, slot_name) = c.split(' ', 2)
93 if slot_name == 'dead': continue
94 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
96 choices += 'OR A SNACK. '
97 choices += '99 TO READ AGAIN. '
99 msg.append((choices, False, None))
104 info = pwd.getpwuid(uid)
106 logging.info('getting pin for uid %d: user not in password file'%uid)
108 if info.pw_dir == None: return False
109 pinfile = os.path.join(info.pw_dir, '.pin')
113 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
116 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
121 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
123 pinstr = f.readline()
125 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
126 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
130 def has_good_pin(uid):
131 return get_pin(uid) != None
133 def verify_user_pin(uid, pin):
134 if get_pin(uid) == pin:
135 info = pwd.getpwuid(uid)
136 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
139 logging.info('refused pin for uid %d'%(uid))
142 def door_open_mode(v):
143 logging.warning("Entering open door mode")
144 v.display("-FEED ME-")
150 if params == 1: # door closed
151 logging.warning('Leaving open door mode')
152 v.display("-YUM YUM!-")
158 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
159 choice = int(random()*len(messages))
160 msg = messages[choice]
161 left = range(len(msg))
162 for i in range(len(msg)):
163 if msg[i] == ' ': left.remove(i)
167 for i in range(0, len(msg)):
173 s += chr(int(random()*26)+ord('A'))
182 return ' '*((LEN-len(str))/2)+str
193 StringIdler(v, text=CREDITS),
194 StringIdler(v, text=str(math.pi) + " "),
195 StringIdler(v, text=str(math.e) + " "),
198 GrayIdler(v,one="*",zero="-"),
199 GrayIdler(v,one="/",zero="\\"),
200 GrayIdler(v,one="X",zero="O"),
201 GrayIdler(v,one="*",zero="-",reorder=1),
202 GrayIdler(v,one="/",zero="\\",reorder=1),
203 GrayIdler(v,one="X",zero="O",reorder=1),
205 idler = choose_idler()
209 idler = idlers[int(random()*len(idlers))]
216 def run_forever(rfh, wfh, options, cf):
217 v = VendingMachine(rfh, wfh)
218 logging.debug('PING is ' + str(v.ping()))
220 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
225 mk = MessageKeeper(v)
226 mk.set_message(GREETING)
227 time_to_autologout = None
230 last_timeout_refresh = None
236 except DispenseDatabaseException, e:
237 logging.error('Database error: '+str(e))
239 if time_to_autologout != None:
240 time_left = time_to_autologout - time()
241 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
242 mk.set_message('LOGOUT: '+str(int(time_left)))
243 last_timeout_refresh = int(time_left)
246 if time_to_autologout != None and time_to_autologout - time() <= 0:
247 time_to_autologout = None
251 mk.set_message(GREETING)
253 if time_to_autologout and not mk.done(): time_to_autologout = None
254 if cur_user == '' and time_to_autologout: time_to_autologout = None
255 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
257 time_to_autologout = time() + 15
259 if time_to_idle == None and cur_user == '':
260 time_to_idle = time() + 5
262 if time_to_idle is not None and cur_user != '': time_to_idle = None
263 if time_to_idle is not None and time() > time_to_idle: idle_step()
264 if time_to_idle is not None and time() > time_to_idle + 300:
265 time_to_idle = time()
272 e = v.next_event(0.05)
277 logging.debug('Got event: ' + repr(e))
283 mk.set_message(GREETING)
284 elif event == SWITCH:
285 # don't care right now.
289 # complicated key handling here:
290 if len(cur_user) < 5:
293 mk.set_message(GREETING)
295 cur_user += chr(key + ord('0'))
296 mk.set_message('UID: '+cur_user)
297 if len(cur_user) == 5:
299 if not has_good_pin(uid):
300 logging.info('user '+cur_user+' has a bad PIN')
302 #[(center('INVALID'), False, 0.7),
303 #(center('PIN'), False, 0.7),
304 #(center('SETUP'), False, 1.0),
305 #(GREETING, False, None)])
307 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
308 (GREETING, False, None)])
313 mk.set_message('PIN: ')
314 logging.info('need pin for user %s'%cur_user)
316 elif len(cur_pin) < PIN_LENGTH:
320 mk.set_message(GREETING)
323 mk.set_message('PIN: ')
325 cur_pin += chr(key + ord('0'))
326 mk.set_message('PIN: '+'X'*len(cur_pin))
327 if len(cur_pin) == PIN_LENGTH:
328 username = verify_user_pin(int(cur_user), int(cur_pin))
332 scroll_options(username, mk, True)
337 [(center('BAD PIN'), False, 1.0),
338 (center('SORRY'), False, 0.5),
339 (GREETING, False, None)])
343 elif len(cur_selection) == 0:
349 [(center('BYE!'), False, 1.5),
350 (GREETING, False, None)])
352 cur_selection += chr(key + ord('0'))
353 mk.set_message('SELECT: '+cur_selection)
354 time_to_autologout = None
355 elif len(cur_selection) == 1:
358 time_to_autologout = None
359 scroll_options(username, mk)
362 cur_selection += chr(key + ord('0'))
363 #make_selection(cur_selection)
364 # XXX this should move somewhere else:
365 if cur_selection == '55':
366 mk.set_message('OPENSESAME')
367 logging.info('dispensing a door for %s'%username)
369 ret = os.system('su - "%s" -c "dispense door"'%username)
371 ret = os.system('dispense door')
373 logging.info('door opened')
374 mk.set_message(center('DOOR OPEN'))
376 logging.warning('user %s tried to dispense a bad door'%username)
377 mk.set_message(center('BAD DOOR'))
379 elif cur_selection == '91':
381 elif cur_selection == '99':
382 scroll_options(username, mk)
385 elif cur_selection[1] == '8':
386 v.display('GOT COKE?')
387 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
389 v.display('HERES A '+cur_selection)
390 v.vend(cur_selection)
392 v.display('THANK YOU')
395 time_to_autologout = time() + 8
397 def connect_to_vend(options, cf):
398 # Open vending machine via LAT?
400 logging.info('Connecting to vending machine using LAT')
401 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
402 rfh, wfh = latclient.get_fh()
404 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
405 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
407 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
408 sock.connect((options.host, options.port))
409 rfh = sock.makefile('r')
410 wfh = sock.makefile('w')
415 from optparse import OptionParser
417 op = OptionParser(usage="%prog [OPTION]...")
418 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')
419 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
420 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
421 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
422 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
423 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
424 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
425 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
426 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
427 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
428 options, args = op.parse_args()
431 op.error('extra command line arguments: ' + ' '.join(args))
436 'DBServer': ('Database', 'Server'),
437 'DBName': ('Database', 'Name'),
438 'DBUser': ('VendingMachine', 'DBUser'),
439 'DBPassword': ('VendingMachine', 'DBPassword'),
441 'ServiceName': ('VendingMachine', 'ServiceName'),
442 'ServicePassword': ('VendingMachine', 'Password'),
444 'ServerName': ('DecServer', 'Name'),
445 'ConnectPassword': ('DecServer', 'ConnectPassword'),
446 'PrivPassword': ('DecServer', 'PrivPassword'),
449 class VendConfigFile:
450 def __init__(self, config_file, options):
452 cp = ConfigParser.ConfigParser()
455 for option in options:
456 section, name = options[option]
457 value = cp.get(section, name)
458 self.__dict__[option] = value
460 except ConfigParser.Error, e:
461 raise SystemExit("Error reading config file "+config_file+": " + str(e))
463 def create_pid_file(name):
465 pid_file = file(name, 'w')
466 pid_file.write('%d\n'%os.getpid())
469 logging.warning('unable to write to pid file '+name+': '+str(e))
472 def do_nothing(signum, stack):
473 signal.signal(signum, do_nothing)
474 def stop_server(signum, stack): raise KeyboardInterrupt
475 signal.signal(signal.SIGHUP, do_nothing)
476 signal.signal(signal.SIGTERM, stop_server)
477 signal.signal(signal.SIGINT, stop_server)
479 options = parse_args()
480 config_opts = VendConfigFile(options.config_file, config_options)
481 if options.daemon: become_daemon()
482 set_up_logging(options)
483 if options.pid_file != '': create_pid_file(options.pid_file)
485 return options, config_opts
487 def clean_up_nicely(options, config_opts):
488 if options.pid_file != '':
490 os.unlink(options.pid_file)
491 logging.debug('Removed pid file '+options.pid_file)
492 except OSError: pass # if we can't delete it, meh
494 def set_up_logging(options):
495 logger = logging.getLogger()
497 if not options.daemon:
498 stderr_logger = logging.StreamHandler(sys.stderr)
499 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
500 logger.addHandler(stderr_logger)
502 if options.log_file != '':
504 file_logger = logging.FileHandler(options.log_file)
505 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
506 logger.addHandler(file_logger)
508 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
510 if options.syslog != None:
511 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
512 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
513 logger.addHandler(sys_logger)
516 logger.setLevel(logging.WARNING)
517 elif options.verbose:
518 logger.setLevel(logging.DEBUG)
520 logger.setLevel(logging.INFO)
523 dev_null = file('/dev/null')
524 fd = dev_null.fileno()
533 raise SystemExit('failed to fork: '+str(e))
535 def do_vend_server(options, config_opts):
538 rfh, wfh = connect_to_vend(options, config_opts)
539 except (LATClientException, socket.error), e:
540 (exc_type, exc_value, exc_traceback) = sys.exc_info()
542 logging.error("Connection error: "+str(exc_type)+" "+str(e))
543 logging.info("Trying again in 5 seconds.")
548 run_forever(rfh, wfh, options, config_opts)
549 except VendingException:
550 logging.error("Connection died, trying again...")
551 logging.info("Trying again in 5 seconds.")
554 if __name__ == '__main__':
555 options, config_opts = set_stuff_up()
558 logging.warning('Starting Vend Server')
559 do_vend_server(options, config_opts)
560 logging.error('Vend Server finished unexpectedly, restarting')
561 except KeyboardInterrupt:
562 logging.info("Killed by signal, cleaning up")
563 clean_up_nicely(options, config_opts)
564 logging.warning("Vend Server stopped")
569 (exc_type, exc_value, exc_traceback) = sys.exc_info()
570 tb = format_tb(exc_traceback, 20)
573 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
574 logging.critical("Message: " + str(exc_value))
575 logging.critical("Traceback:")
577 for line in event.split('\n'):
578 logging.critical(' '+line)
579 logging.critical("This message should be considered a bug in the Vend Server.")
580 logging.critical("Please report this to someone who can fix it.")
582 logging.warning("Trying again anyway (might not help, but hey...)")