7 import sys, os, string, re, pwd, signal
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.
31 GREETING = 'UCC SNACKS'
38 class DispenseDatabaseException(Exception): pass
40 class DispenseDatabase:
41 def __init__(self, vending_machine, host, name, user, password):
42 self.vending_machine = vending_machine
43 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
44 self.db.query('LISTEN vend_requests')
46 def process_requests(self):
47 logging.debug('database processing')
48 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
50 outstanding = self.db.query(query).getresult()
51 except (pg.error,), db_err:
52 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
53 for (id, slot) in outstanding:
54 (worked, code, string) = self.vending_machine.vend(slot)
55 logging.debug (str((worked, code, string)))
57 query = 'SELECT vend_success(%s)'%id
58 self.db.query(query).getresult()
60 query = 'SELECT vend_failed(%s)'%id
61 self.db.query(query).getresult()
63 def handle_events(self):
64 notifier = self.db.getnotify()
65 while notifier is not None:
66 self.process_requests()
67 notify = self.db.getnotify()
69 def scroll_options(username, mk, welcome = False):
71 msg = [(center('WELCOME'), False, 0.8),
72 (center(username), False, 0.8)]
75 choices = ' '*10+'CHOICES: '
77 coke_machine = file('/home/other/coke/coke_contents')
78 cokes = coke_machine.readlines()
85 (slot_num, price, slot_name) = c.split(' ', 2)
86 if slot_name == 'dead': continue
87 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
89 choices += 'OR A SNACK. '
90 choices += '99 TO READ AGAIN. '
92 msg.append((choices, False, None))
97 info = pwd.getpwuid(uid)
99 logging.info('getting pin for uid %d: user not in password file'%uid)
101 if info.pw_dir == None: return False
102 pinfile = os.path.join(info.pw_dir, '.pin')
106 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
109 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
114 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
116 pinstr = f.readline()
118 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
119 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
123 def has_good_pin(uid):
124 return get_pin(uid) != None
126 def verify_user_pin(uid, pin):
127 if get_pin(uid) == pin:
128 info = pwd.getpwuid(uid)
129 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
132 logging.info('refused pin for uid %d'%(uid))
135 def door_open_mode(v):
136 logging.warning("Entering open door mode")
137 v.display("-FEED ME-")
143 if params == 1: # door closed
144 logging.warning('Leaving open door mode')
145 v.display("-YUM YUM!-")
151 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
152 choice = int(random()*len(messages))
153 msg = messages[choice]
154 left = range(len(msg))
155 for i in range(len(msg)):
156 if msg[i] == ' ': left.remove(i)
160 for i in range(0, len(msg)):
166 s += chr(int(random()*26)+ord('A'))
175 return ' '*((LEN-len(str))/2)+str
186 StringIdler(v, text=CREDITS),
188 GrayIdler(v,one="*",zero="-"),
189 GrayIdler(v,one="/",zero="\\"),
190 GrayIdler(v,one="X",zero="O"),
191 GrayIdler(v,one="*",zero="-",reorder=1),
192 GrayIdler(v,one="/",zero="\\",reorder=1),
193 GrayIdler(v,one="X",zero="O",reorder=1),
195 idler = choose_idler()
199 idler = idlers[int(random()*len(idlers))]
206 def run_forever(rfh, wfh, options, cf):
207 v = VendingMachine(rfh, wfh)
208 logging.debug('PING is ' + str(v.ping()))
210 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
215 mk = MessageKeeper(v)
216 mk.set_message(GREETING)
217 time_to_autologout = None
220 last_timeout_refresh = None
226 except DispenseDatabaseException, e:
227 logging.error('Database error: '+str(e))
229 if time_to_autologout != None:
230 time_left = time_to_autologout - time()
231 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
232 mk.set_message('LOGOUT: '+str(int(time_left)))
233 last_timeout_refresh = int(time_left)
236 if time_to_autologout != None and time_to_autologout - time() <= 0:
237 time_to_autologout = None
241 mk.set_message(GREETING)
243 if time_to_autologout and not mk.done(): time_to_autologout = None
244 if cur_user == '' and time_to_autologout: time_to_autologout = None
245 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
247 time_to_autologout = time() + 15
249 if time_to_idle == None and cur_user == '':
250 time_to_idle = time() + 5
252 if time_to_idle is not None and cur_user != '': time_to_idle = None
253 if time_to_idle is not None and time() > time_to_idle: idle_step()
254 if time_to_idle is not None and time() > time_to_idle + 300:
255 time_to_idle = time()
262 e = v.next_event(0.05)
267 logging.debug('Got event: ' + repr(e))
273 mk.set_message(GREETING)
274 elif event == SWITCH:
275 # don't care right now.
279 # complicated key handling here:
280 if len(cur_user) < 5:
283 mk.set_message(GREETING)
285 cur_user += chr(key + ord('0'))
286 mk.set_message('UID: '+cur_user)
287 if len(cur_user) == 5:
289 if not has_good_pin(uid):
290 logging.info('user '+cur_user+' has a bad PIN')
292 #[(center('INVALID'), False, 0.7),
293 #(center('PIN'), False, 0.7),
294 #(center('SETUP'), False, 1.0),
295 #(GREETING, False, None)])
297 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
298 (GREETING, False, None)])
303 mk.set_message('PIN: ')
304 logging.info('need pin for user %s'%cur_user)
306 elif len(cur_pin) < PIN_LENGTH:
310 mk.set_message(GREETING)
313 mk.set_message('PIN: ')
315 cur_pin += chr(key + ord('0'))
316 mk.set_message('PIN: '+'X'*len(cur_pin))
317 if len(cur_pin) == PIN_LENGTH:
318 username = verify_user_pin(int(cur_user), int(cur_pin))
322 scroll_options(username, mk, True)
327 [(center('BAD PIN'), False, 1.0),
328 (center('SORRY'), False, 0.5),
329 (GREETING, False, None)])
333 elif len(cur_selection) == 0:
339 [(center('BYE!'), False, 1.5),
340 (GREETING, False, None)])
342 cur_selection += chr(key + ord('0'))
343 mk.set_message('SELECT: '+cur_selection)
344 time_to_autologout = None
345 elif len(cur_selection) == 1:
348 time_to_autologout = None
349 scroll_options(username, mk)
352 cur_selection += chr(key + ord('0'))
353 #make_selection(cur_selection)
354 # XXX this should move somewhere else:
355 if cur_selection == '55':
356 mk.set_message('OPENSESAME')
357 logging.info('dispensing a door for %s'%username)
359 ret = os.system('su - "%s" -c "dispense door"'%username)
361 ret = os.system('dispense door')
363 logging.info('door opened')
364 mk.set_message(center('DOOR OPEN'))
366 logging.warning('user %s tried to dispense a bad door'%username)
367 mk.set_message(center('BAD DOOR'))
369 elif cur_selection == '91':
371 elif cur_selection == '99':
372 scroll_options(username, mk)
375 elif cur_selection[1] == '8':
376 v.display('GOT COKE?')
377 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
379 v.display('HERES A '+cur_selection)
380 v.vend(cur_selection)
382 v.display('THANK YOU')
385 time_to_autologout = time() + 8
387 def connect_to_vend(options, cf):
388 # Open vending machine via LAT?
390 logging.info('Connecting to vending machine using LAT')
391 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
392 rfh, wfh = latclient.get_fh()
394 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
395 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
397 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
398 sock.connect((options.host, options.port))
399 rfh = sock.makefile('r')
400 wfh = sock.makefile('w')
405 from optparse import OptionParser
407 op = OptionParser(usage="%prog [OPTION]...")
408 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')
409 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
410 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
411 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
412 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
413 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
414 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
415 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
416 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
417 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
418 options, args = op.parse_args()
421 op.error('extra command line arguments: ' + ' '.join(args))
426 'DBServer': ('Database', 'Server'),
427 'DBName': ('Database', 'Name'),
428 'DBUser': ('VendingMachine', 'DBUser'),
429 'DBPassword': ('VendingMachine', 'DBPassword'),
431 'ServiceName': ('VendingMachine', 'ServiceName'),
432 'ServicePassword': ('VendingMachine', 'Password'),
434 'ServerName': ('DecServer', 'Name'),
435 'ConnectPassword': ('DecServer', 'ConnectPassword'),
436 'PrivPassword': ('DecServer', 'PrivPassword'),
439 class VendConfigFile:
440 def __init__(self, config_file, options):
442 cp = ConfigParser.ConfigParser()
445 for option in options:
446 section, name = options[option]
447 value = cp.get(section, name)
448 self.__dict__[option] = value
450 except ConfigParser.Error, e:
451 raise SystemExit("Error reading config file "+config_file+": " + str(e))
453 def create_pid_file(name):
455 pid_file = file(name, 'w')
456 pid_file.write('%d\n'%os.getpid())
459 logging.warning('unable to write to pid file '+name+': '+str(e))
462 def do_nothing(signum, stack):
463 signal.signal(signum, do_nothing)
464 def stop_server(signum, stack): raise KeyboardInterrupt
465 signal.signal(signal.SIGHUP, do_nothing)
466 signal.signal(signal.SIGTERM, stop_server)
467 signal.signal(signal.SIGINT, stop_server)
469 options = parse_args()
470 config_opts = VendConfigFile(options.config_file, config_options)
471 if options.daemon: become_daemon()
472 set_up_logging(options)
473 if options.pid_file != '': create_pid_file(options.pid_file)
475 return options, config_opts
477 def clean_up_nicely(options, config_opts):
478 if options.pid_file != '':
480 os.unlink(options.pid_file)
481 logging.debug('Removed pid file '+options.pid_file)
482 except OSError: pass # if we can't delete it, meh
484 def set_up_logging(options):
485 logger = logging.getLogger()
487 if not options.daemon:
488 stderr_logger = logging.StreamHandler(sys.stderr)
489 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
490 logger.addHandler(stderr_logger)
492 if options.log_file != '':
494 file_logger = logging.FileHandler(options.log_file)
495 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
496 logger.addHandler(file_logger)
498 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
500 if options.syslog != None:
501 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
502 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
503 logger.addHandler(sys_logger)
506 logger.setLevel(logging.WARNING)
507 elif options.verbose:
508 logger.setLevel(logging.DEBUG)
510 logger.setLevel(logging.INFO)
513 dev_null = file('/dev/null')
514 fd = dev_null.fileno()
523 raise SystemExit('failed to fork: '+str(e))
525 def do_vend_server(options, config_opts):
528 rfh, wfh = connect_to_vend(options, config_opts)
529 except (LATClientException, socket.error), e:
530 (exc_type, exc_value, exc_traceback) = sys.exc_info()
532 logging.error("Connection error: "+str(exc_type)+" "+str(e))
533 logging.info("Trying again in 5 seconds.")
538 run_forever(rfh, wfh, options, config_opts)
539 except VendingException:
540 logging.error("Connection died, trying again...")
541 logging.info("Trying again in 5 seconds.")
544 if __name__ == '__main__':
545 options, config_opts = set_stuff_up()
548 logging.warning('Starting Vend Server')
549 do_vend_server(options, config_opts)
550 logging.error('Vend Server finished unexpectedly, restarting')
551 except KeyboardInterrupt:
552 logging.info("Killed by signal, cleaning up")
553 clean_up_nicely(options, config_opts)
554 logging.warning("Vend Server stopped")
559 (exc_type, exc_value, exc_traceback) = sys.exc_info()
560 tb = format_tb(exc_traceback, 20)
563 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
564 logging.critical("Message: " + str(exc_value))
565 logging.critical("Traceback:")
567 for line in event.split('\n'):
568 logging.critical(' '+line)
569 logging.critical("This message should be considered a bug in the Vend Server.")
570 logging.critical("Please report this to someone who can fix it.")
572 logging.warning("Trying again anyway (might not help, but hey...)")