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
193 StringIdler(v, text="Kill 'em all", repeat=False),
194 StringIdler(v, text=CREDITS),
195 StringIdler(v, text=str(math.pi) + " "),
196 StringIdler(v, text=str(math.e) + " "),
200 GrayIdler(v,one="*",zero="-"),
201 GrayIdler(v,one="/",zero="\\"),
202 GrayIdler(v,one="X",zero="O"),
203 GrayIdler(v,one="*",zero="-",reorder=1),
204 GrayIdler(v,one="/",zero="\\",reorder=1),
205 GrayIdler(v,one="X",zero="O",reorder=1),
207 idler = choose_idler()
211 idler = idlers[int(random()*len(idlers))]
220 def run_forever(rfh, wfh, options, cf):
221 v = VendingMachine(rfh, wfh)
222 logging.debug('PING is ' + str(v.ping()))
224 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
229 mk = MessageKeeper(v)
230 mk.set_message(GREETING)
231 time_to_autologout = None
234 last_timeout_refresh = None
240 except DispenseDatabaseException, e:
241 logging.error('Database error: '+str(e))
243 if time_to_autologout != None:
244 time_left = time_to_autologout - time()
245 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
246 mk.set_message('LOGOUT: '+str(int(time_left)))
247 last_timeout_refresh = int(time_left)
250 if time_to_autologout != None and time_to_autologout - time() <= 0:
251 time_to_autologout = None
255 mk.set_message(GREETING)
257 if time_to_autologout and not mk.done(): time_to_autologout = None
258 if cur_user == '' and time_to_autologout: time_to_autologout = None
259 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
261 time_to_autologout = time() + 15
263 if time_to_idle == None and cur_user == '':
264 time_to_idle = time() + 5
266 if time_to_idle is not None and cur_user != '': time_to_idle = None
267 if time_to_idle is not None and time() > time_to_idle: idle_step()
268 if time_to_idle is not None and time() > time_to_idle + 300:
269 time_to_idle = time()
276 e = v.next_event(0.05)
281 logging.debug('Got event: ' + repr(e))
287 mk.set_message(GREETING)
288 elif event == SWITCH:
289 # don't care right now.
293 # complicated key handling here:
294 if len(cur_user) < 5:
297 mk.set_message(GREETING)
299 cur_user += chr(key + ord('0'))
300 mk.set_message('UID: '+cur_user)
301 if len(cur_user) == 5:
303 if not has_good_pin(uid):
304 logging.info('user '+cur_user+' has a bad PIN')
306 #[(center('INVALID'), False, 0.7),
307 #(center('PIN'), False, 0.7),
308 #(center('SETUP'), False, 1.0),
309 #(GREETING, False, None)])
311 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
312 (GREETING, False, None)])
317 mk.set_message('PIN: ')
318 logging.info('need pin for user %s'%cur_user)
320 elif len(cur_pin) < PIN_LENGTH:
324 mk.set_message(GREETING)
327 mk.set_message('PIN: ')
329 cur_pin += chr(key + ord('0'))
330 mk.set_message('PIN: '+'X'*len(cur_pin))
331 if len(cur_pin) == PIN_LENGTH:
332 username = verify_user_pin(int(cur_user), int(cur_pin))
336 scroll_options(username, mk, True)
341 [(center('BAD PIN'), False, 1.0),
342 (center('SORRY'), False, 0.5),
343 (GREETING, False, None)])
347 elif len(cur_selection) == 0:
353 [(center('BYE!'), False, 1.5),
354 (GREETING, False, None)])
356 cur_selection += chr(key + ord('0'))
357 mk.set_message('SELECT: '+cur_selection)
358 time_to_autologout = None
359 elif len(cur_selection) == 1:
362 time_to_autologout = None
363 scroll_options(username, mk)
366 cur_selection += chr(key + ord('0'))
367 #make_selection(cur_selection)
368 # XXX this should move somewhere else:
369 if cur_selection == '55':
370 mk.set_message('OPENSESAME')
371 logging.info('dispensing a door for %s'%username)
373 ret = os.system('su - "%s" -c "dispense door"'%username)
375 ret = os.system('dispense door')
377 logging.info('door opened')
378 mk.set_message(center('DOOR OPEN'))
380 logging.warning('user %s tried to dispense a bad door'%username)
381 mk.set_message(center('BAD DOOR'))
383 elif cur_selection == '91':
385 elif cur_selection == '99':
386 scroll_options(username, mk)
389 elif cur_selection[1] == '8':
390 v.display('GOT COKE?')
391 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
393 v.display('HERES A '+cur_selection)
394 v.vend(cur_selection)
396 v.display('THANK YOU')
399 time_to_autologout = time() + 8
401 def connect_to_vend(options, cf):
402 # Open vending machine via LAT?
404 logging.info('Connecting to vending machine using LAT')
405 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
406 rfh, wfh = latclient.get_fh()
408 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
409 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
411 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
412 sock.connect((options.host, options.port))
413 rfh = sock.makefile('r')
414 wfh = sock.makefile('w')
419 from optparse import OptionParser
421 op = OptionParser(usage="%prog [OPTION]...")
422 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')
423 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
424 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
425 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
426 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
427 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
428 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
429 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
430 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
431 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
432 options, args = op.parse_args()
435 op.error('extra command line arguments: ' + ' '.join(args))
440 'DBServer': ('Database', 'Server'),
441 'DBName': ('Database', 'Name'),
442 'DBUser': ('VendingMachine', 'DBUser'),
443 'DBPassword': ('VendingMachine', 'DBPassword'),
445 'ServiceName': ('VendingMachine', 'ServiceName'),
446 'ServicePassword': ('VendingMachine', 'Password'),
448 'ServerName': ('DecServer', 'Name'),
449 'ConnectPassword': ('DecServer', 'ConnectPassword'),
450 'PrivPassword': ('DecServer', 'PrivPassword'),
453 class VendConfigFile:
454 def __init__(self, config_file, options):
456 cp = ConfigParser.ConfigParser()
459 for option in options:
460 section, name = options[option]
461 value = cp.get(section, name)
462 self.__dict__[option] = value
464 except ConfigParser.Error, e:
465 raise SystemExit("Error reading config file "+config_file+": " + str(e))
467 def create_pid_file(name):
469 pid_file = file(name, 'w')
470 pid_file.write('%d\n'%os.getpid())
473 logging.warning('unable to write to pid file '+name+': '+str(e))
476 def do_nothing(signum, stack):
477 signal.signal(signum, do_nothing)
478 def stop_server(signum, stack): raise KeyboardInterrupt
479 signal.signal(signal.SIGHUP, do_nothing)
480 signal.signal(signal.SIGTERM, stop_server)
481 signal.signal(signal.SIGINT, stop_server)
483 options = parse_args()
484 config_opts = VendConfigFile(options.config_file, config_options)
485 if options.daemon: become_daemon()
486 set_up_logging(options)
487 if options.pid_file != '': create_pid_file(options.pid_file)
489 return options, config_opts
491 def clean_up_nicely(options, config_opts):
492 if options.pid_file != '':
494 os.unlink(options.pid_file)
495 logging.debug('Removed pid file '+options.pid_file)
496 except OSError: pass # if we can't delete it, meh
498 def set_up_logging(options):
499 logger = logging.getLogger()
501 if not options.daemon:
502 stderr_logger = logging.StreamHandler(sys.stderr)
503 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
504 logger.addHandler(stderr_logger)
506 if options.log_file != '':
508 file_logger = logging.FileHandler(options.log_file)
509 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
510 logger.addHandler(file_logger)
512 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
514 if options.syslog != None:
515 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
516 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
517 logger.addHandler(sys_logger)
520 logger.setLevel(logging.WARNING)
521 elif options.verbose:
522 logger.setLevel(logging.DEBUG)
524 logger.setLevel(logging.INFO)
527 dev_null = file('/dev/null')
528 fd = dev_null.fileno()
537 raise SystemExit('failed to fork: '+str(e))
539 def do_vend_server(options, config_opts):
542 rfh, wfh = connect_to_vend(options, config_opts)
543 except (LATClientException, socket.error), e:
544 (exc_type, exc_value, exc_traceback) = sys.exc_info()
546 logging.error("Connection error: "+str(exc_type)+" "+str(e))
547 logging.info("Trying again in 5 seconds.")
552 run_forever(rfh, wfh, options, config_opts)
553 except VendingException:
554 logging.error("Connection died, trying again...")
555 logging.info("Trying again in 5 seconds.")
558 if __name__ == '__main__':
559 options, config_opts = set_stuff_up()
562 logging.warning('Starting Vend Server')
563 do_vend_server(options, config_opts)
564 logging.error('Vend Server finished unexpectedly, restarting')
565 except KeyboardInterrupt:
566 logging.info("Killed by signal, cleaning up")
567 clean_up_nicely(options, config_opts)
568 logging.warning("Vend Server stopped")
573 (exc_type, exc_value, exc_traceback) = sys.exc_info()
574 tb = format_tb(exc_traceback, 20)
577 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
578 logging.critical("Message: " + str(exc_value))
579 logging.critical("Traceback:")
581 for line in event.split('\n'):
582 logging.critical(' '+line)
583 logging.critical("This message should be considered a bug in the Vend Server.")
584 logging.critical("Please report this to someone who can fix it.")
586 logging.warning("Trying again anyway (might not help, but hey...)")