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,ClockIdler
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
194 StringIdler(v, text=CREDITS),
195 StringIdler(v, text=str(math.pi) + " "),
196 StringIdler(v, text=str(math.e) + " "),
199 GrayIdler(v,one="*",zero="-"),
200 GrayIdler(v,one="/",zero="\\"),
201 GrayIdler(v,one="X",zero="O"),
202 GrayIdler(v,one="*",zero="-",reorder=1),
203 GrayIdler(v,one="/",zero="\\",reorder=1),
204 GrayIdler(v,one="X",zero="O",reorder=1),
206 idler = choose_idler()
210 idler = idlers[int(random()*len(idlers))]
219 def run_forever(rfh, wfh, options, cf):
220 v = VendingMachine(rfh, wfh)
221 logging.debug('PING is ' + str(v.ping()))
223 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
228 mk = MessageKeeper(v)
229 mk.set_message(GREETING)
230 time_to_autologout = None
233 last_timeout_refresh = None
239 except DispenseDatabaseException, e:
240 logging.error('Database error: '+str(e))
242 if time_to_autologout != None:
243 time_left = time_to_autologout - time()
244 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
245 mk.set_message('LOGOUT: '+str(int(time_left)))
246 last_timeout_refresh = int(time_left)
249 if time_to_autologout != None and time_to_autologout - time() <= 0:
250 time_to_autologout = None
254 mk.set_message(GREETING)
256 if time_to_autologout and not mk.done(): time_to_autologout = None
257 if cur_user == '' and time_to_autologout: time_to_autologout = None
258 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
260 time_to_autologout = time() + 15
262 if time_to_idle == None and cur_user == '':
263 time_to_idle = time() + 5
265 if time_to_idle is not None and cur_user != '': time_to_idle = None
266 if time_to_idle is not None and time() > time_to_idle: idle_step()
267 if time_to_idle is not None and time() > time_to_idle + 300:
268 time_to_idle = time()
275 e = v.next_event(0.05)
280 logging.debug('Got event: ' + repr(e))
286 mk.set_message(GREETING)
287 elif event == SWITCH:
288 # don't care right now.
292 # complicated key handling here:
293 if len(cur_user) < 5:
296 mk.set_message(GREETING)
298 cur_user += chr(key + ord('0'))
299 mk.set_message('UID: '+cur_user)
300 if len(cur_user) == 5:
302 if not has_good_pin(uid):
303 logging.info('user '+cur_user+' has a bad PIN')
305 #[(center('INVALID'), False, 0.7),
306 #(center('PIN'), False, 0.7),
307 #(center('SETUP'), False, 1.0),
308 #(GREETING, False, None)])
310 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
311 (GREETING, False, None)])
316 mk.set_message('PIN: ')
317 logging.info('need pin for user %s'%cur_user)
319 elif len(cur_pin) < PIN_LENGTH:
323 mk.set_message(GREETING)
326 mk.set_message('PIN: ')
328 cur_pin += chr(key + ord('0'))
329 mk.set_message('PIN: '+'X'*len(cur_pin))
330 if len(cur_pin) == PIN_LENGTH:
331 username = verify_user_pin(int(cur_user), int(cur_pin))
335 scroll_options(username, mk, True)
340 [(center('BAD PIN'), False, 1.0),
341 (center('SORRY'), False, 0.5),
342 (GREETING, False, None)])
346 elif len(cur_selection) == 0:
352 [(center('BYE!'), False, 1.5),
353 (GREETING, False, None)])
355 cur_selection += chr(key + ord('0'))
356 mk.set_message('SELECT: '+cur_selection)
357 time_to_autologout = None
358 elif len(cur_selection) == 1:
361 time_to_autologout = None
362 scroll_options(username, mk)
365 cur_selection += chr(key + ord('0'))
366 #make_selection(cur_selection)
367 # XXX this should move somewhere else:
368 if cur_selection == '55':
369 mk.set_message('OPENSESAME')
370 logging.info('dispensing a door for %s'%username)
372 ret = os.system('su - "%s" -c "dispense door"'%username)
374 ret = os.system('dispense door')
376 logging.info('door opened')
377 mk.set_message(center('DOOR OPEN'))
379 logging.warning('user %s tried to dispense a bad door'%username)
380 mk.set_message(center('BAD DOOR'))
382 elif cur_selection == '91':
384 elif cur_selection == '99':
385 scroll_options(username, mk)
388 elif cur_selection[1] == '8':
389 v.display('GOT COKE?')
390 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
392 v.display('HERES A '+cur_selection)
393 v.vend(cur_selection)
395 v.display('THANK YOU')
398 time_to_autologout = time() + 8
400 def connect_to_vend(options, cf):
401 # Open vending machine via LAT?
403 logging.info('Connecting to vending machine using LAT')
404 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
405 rfh, wfh = latclient.get_fh()
407 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
408 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
410 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
411 sock.connect((options.host, options.port))
412 rfh = sock.makefile('r')
413 wfh = sock.makefile('w')
418 from optparse import OptionParser
420 op = OptionParser(usage="%prog [OPTION]...")
421 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')
422 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
423 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
424 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
425 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
426 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
427 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
428 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
429 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
430 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
431 options, args = op.parse_args()
434 op.error('extra command line arguments: ' + ' '.join(args))
439 'DBServer': ('Database', 'Server'),
440 'DBName': ('Database', 'Name'),
441 'DBUser': ('VendingMachine', 'DBUser'),
442 'DBPassword': ('VendingMachine', 'DBPassword'),
444 'ServiceName': ('VendingMachine', 'ServiceName'),
445 'ServicePassword': ('VendingMachine', 'Password'),
447 'ServerName': ('DecServer', 'Name'),
448 'ConnectPassword': ('DecServer', 'ConnectPassword'),
449 'PrivPassword': ('DecServer', 'PrivPassword'),
452 class VendConfigFile:
453 def __init__(self, config_file, options):
455 cp = ConfigParser.ConfigParser()
458 for option in options:
459 section, name = options[option]
460 value = cp.get(section, name)
461 self.__dict__[option] = value
463 except ConfigParser.Error, e:
464 raise SystemExit("Error reading config file "+config_file+": " + str(e))
466 def create_pid_file(name):
468 pid_file = file(name, 'w')
469 pid_file.write('%d\n'%os.getpid())
472 logging.warning('unable to write to pid file '+name+': '+str(e))
475 def do_nothing(signum, stack):
476 signal.signal(signum, do_nothing)
477 def stop_server(signum, stack): raise KeyboardInterrupt
478 signal.signal(signal.SIGHUP, do_nothing)
479 signal.signal(signal.SIGTERM, stop_server)
480 signal.signal(signal.SIGINT, stop_server)
482 options = parse_args()
483 config_opts = VendConfigFile(options.config_file, config_options)
484 if options.daemon: become_daemon()
485 set_up_logging(options)
486 if options.pid_file != '': create_pid_file(options.pid_file)
488 return options, config_opts
490 def clean_up_nicely(options, config_opts):
491 if options.pid_file != '':
493 os.unlink(options.pid_file)
494 logging.debug('Removed pid file '+options.pid_file)
495 except OSError: pass # if we can't delete it, meh
497 def set_up_logging(options):
498 logger = logging.getLogger()
500 if not options.daemon:
501 stderr_logger = logging.StreamHandler(sys.stderr)
502 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
503 logger.addHandler(stderr_logger)
505 if options.log_file != '':
507 file_logger = logging.FileHandler(options.log_file)
508 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
509 logger.addHandler(file_logger)
511 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
513 if options.syslog != None:
514 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
515 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
516 logger.addHandler(sys_logger)
519 logger.setLevel(logging.WARNING)
520 elif options.verbose:
521 logger.setLevel(logging.DEBUG)
523 logger.setLevel(logging.INFO)
526 dev_null = file('/dev/null')
527 fd = dev_null.fileno()
536 raise SystemExit('failed to fork: '+str(e))
538 def do_vend_server(options, config_opts):
541 rfh, wfh = connect_to_vend(options, config_opts)
542 except (LATClientException, socket.error), e:
543 (exc_type, exc_value, exc_traceback) = sys.exc_info()
545 logging.error("Connection error: "+str(exc_type)+" "+str(e))
546 logging.info("Trying again in 5 seconds.")
551 run_forever(rfh, wfh, options, config_opts)
552 except VendingException:
553 logging.error("Connection died, trying again...")
554 logging.info("Trying again in 5 seconds.")
557 if __name__ == '__main__':
558 options, config_opts = set_stuff_up()
561 logging.warning('Starting Vend Server')
562 do_vend_server(options, config_opts)
563 logging.error('Vend Server finished unexpectedly, restarting')
564 except KeyboardInterrupt:
565 logging.info("Killed by signal, cleaning up")
566 clean_up_nicely(options, config_opts)
567 logging.warning("Vend Server stopped")
572 (exc_type, exc_value, exc_traceback) = sys.exc_info()
573 tb = format_tb(exc_traceback, 20)
576 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
577 logging.critical("Message: " + str(exc_value))
578 logging.critical("Traceback:")
580 for line in event.split('\n'):
581 logging.critical(' '+line)
582 logging.critical("This message should be considered a bug in the Vend Server.")
583 logging.critical("Please report this to someone who can fix it.")
585 logging.warning("Trying again anyway (might not help, but hey...)")