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,FortuneIdler,FileIdler
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 GrayIdler(v,one="*",zero="-"),
195 StringIdler(v, text=CREDITS),
196 GrayIdler(v,one="/",zero="\\"),
197 FileIdler(v, '/etc/passwd'),
198 GrayIdler(v,one="X",zero="O"),
199 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
200 GrayIdler(v,one="*",zero="-",reorder=1),
201 StringIdler(v, text=str(math.pi) + " "),
202 GrayIdler(v,one="/",zero="\\",reorder=1),
203 StringIdler(v, text=str(math.e) + " "),
204 GrayIdler(v,one="X",zero="O",reorder=1),
212 idler = choose_idler()
219 iiindex = idlers.index(idler)
223 move = int(random()*len(idlers)) + 1
226 idler = idlers[( (iiindex + 1) % iilen)]
227 move = move - idler.affinity()
237 def run_forever(rfh, wfh, options, cf):
238 v = VendingMachine(rfh, wfh)
239 logging.debug('PING is ' + str(v.ping()))
241 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
246 mk = MessageKeeper(v)
247 mk.set_message(GREETING)
248 time_to_autologout = None
251 last_timeout_refresh = None
257 except DispenseDatabaseException, e:
258 logging.error('Database error: '+str(e))
260 if time_to_autologout != None:
261 time_left = time_to_autologout - time()
262 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
263 mk.set_message('LOGOUT: '+str(int(time_left)))
264 last_timeout_refresh = int(time_left)
267 if time_to_autologout != None and time_to_autologout - time() <= 0:
268 time_to_autologout = None
272 mk.set_message(GREETING)
274 if time_to_autologout and not mk.done(): time_to_autologout = None
275 if cur_user == '' and time_to_autologout: time_to_autologout = None
276 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
278 time_to_autologout = time() + 15
280 if time_to_idle == None and cur_user == '':
281 time_to_idle = time() + 5
283 if time_to_idle is not None and cur_user != '': time_to_idle = None
284 if time_to_idle is not None and time() > time_to_idle: idle_step()
285 if time_to_idle is not None and time() > time_to_idle + 300:
286 time_to_idle = time()
293 e = v.next_event(0.05)
298 logging.debug('Got event: ' + repr(e))
304 mk.set_message(GREETING)
305 elif event == SWITCH:
306 # don't care right now.
310 # complicated key handling here:
311 if len(cur_user) < 5:
314 mk.set_message(GREETING)
316 cur_user += chr(key + ord('0'))
317 mk.set_message('UID: '+cur_user)
318 if len(cur_user) == 5:
320 if not has_good_pin(uid):
321 logging.info('user '+cur_user+' has a bad PIN')
323 #[(center('INVALID'), False, 0.7),
324 #(center('PIN'), False, 0.7),
325 #(center('SETUP'), False, 1.0),
326 #(GREETING, False, None)])
328 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
329 (GREETING, False, None)])
334 mk.set_message('PIN: ')
335 logging.info('need pin for user %s'%cur_user)
337 elif len(cur_pin) < PIN_LENGTH:
341 mk.set_message(GREETING)
344 mk.set_message('PIN: ')
346 cur_pin += chr(key + ord('0'))
347 mk.set_message('PIN: '+'X'*len(cur_pin))
348 if len(cur_pin) == PIN_LENGTH:
349 username = verify_user_pin(int(cur_user), int(cur_pin))
353 scroll_options(username, mk, True)
358 [(center('BAD PIN'), False, 1.0),
359 (center('SORRY'), False, 0.5),
360 (GREETING, False, None)])
364 elif len(cur_selection) == 0:
370 [(center('BYE!'), False, 1.5),
371 (GREETING, False, None)])
373 cur_selection += chr(key + ord('0'))
374 mk.set_message('SELECT: '+cur_selection)
375 time_to_autologout = None
376 elif len(cur_selection) == 1:
379 time_to_autologout = None
380 scroll_options(username, mk)
383 cur_selection += chr(key + ord('0'))
384 #make_selection(cur_selection)
385 # XXX this should move somewhere else:
386 if cur_selection == '55':
387 mk.set_message('OPENSESAME')
388 logging.info('dispensing a door for %s'%username)
390 ret = os.system('su - "%s" -c "dispense door"'%username)
392 ret = os.system('dispense door')
394 logging.info('door opened')
395 mk.set_message(center('DOOR OPEN'))
397 logging.warning('user %s tried to dispense a bad door'%username)
398 mk.set_message(center('BAD DOOR'))
400 elif cur_selection == '91':
402 elif cur_selection == '99':
403 scroll_options(username, mk)
406 elif cur_selection[1] == '8':
407 v.display('GOT COKE?')
408 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
410 v.display('HERES A '+cur_selection)
411 v.vend(cur_selection)
413 v.display('THANK YOU')
416 time_to_autologout = time() + 8
418 def connect_to_vend(options, cf):
419 # Open vending machine via LAT?
421 logging.info('Connecting to vending machine using LAT')
422 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
423 rfh, wfh = latclient.get_fh()
425 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
426 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
428 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
429 sock.connect((options.host, options.port))
430 rfh = sock.makefile('r')
431 wfh = sock.makefile('w')
436 from optparse import OptionParser
438 op = OptionParser(usage="%prog [OPTION]...")
439 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')
440 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
441 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
442 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
443 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
444 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
445 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
446 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
447 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
448 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
449 options, args = op.parse_args()
452 op.error('extra command line arguments: ' + ' '.join(args))
457 'DBServer': ('Database', 'Server'),
458 'DBName': ('Database', 'Name'),
459 'DBUser': ('VendingMachine', 'DBUser'),
460 'DBPassword': ('VendingMachine', 'DBPassword'),
462 'ServiceName': ('VendingMachine', 'ServiceName'),
463 'ServicePassword': ('VendingMachine', 'Password'),
465 'ServerName': ('DecServer', 'Name'),
466 'ConnectPassword': ('DecServer', 'ConnectPassword'),
467 'PrivPassword': ('DecServer', 'PrivPassword'),
470 class VendConfigFile:
471 def __init__(self, config_file, options):
473 cp = ConfigParser.ConfigParser()
476 for option in options:
477 section, name = options[option]
478 value = cp.get(section, name)
479 self.__dict__[option] = value
481 except ConfigParser.Error, e:
482 raise SystemExit("Error reading config file "+config_file+": " + str(e))
484 def create_pid_file(name):
486 pid_file = file(name, 'w')
487 pid_file.write('%d\n'%os.getpid())
490 logging.warning('unable to write to pid file '+name+': '+str(e))
493 def do_nothing(signum, stack):
494 signal.signal(signum, do_nothing)
495 def stop_server(signum, stack): raise KeyboardInterrupt
496 signal.signal(signal.SIGHUP, do_nothing)
497 signal.signal(signal.SIGTERM, stop_server)
498 signal.signal(signal.SIGINT, stop_server)
500 options = parse_args()
501 config_opts = VendConfigFile(options.config_file, config_options)
502 if options.daemon: become_daemon()
503 set_up_logging(options)
504 if options.pid_file != '': create_pid_file(options.pid_file)
506 return options, config_opts
508 def clean_up_nicely(options, config_opts):
509 if options.pid_file != '':
511 os.unlink(options.pid_file)
512 logging.debug('Removed pid file '+options.pid_file)
513 except OSError: pass # if we can't delete it, meh
515 def set_up_logging(options):
516 logger = logging.getLogger()
518 if not options.daemon:
519 stderr_logger = logging.StreamHandler(sys.stderr)
520 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
521 logger.addHandler(stderr_logger)
523 if options.log_file != '':
525 file_logger = logging.FileHandler(options.log_file)
526 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
527 logger.addHandler(file_logger)
529 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
531 if options.syslog != None:
532 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
533 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
534 logger.addHandler(sys_logger)
537 logger.setLevel(logging.WARNING)
538 elif options.verbose:
539 logger.setLevel(logging.DEBUG)
541 logger.setLevel(logging.INFO)
544 dev_null = file('/dev/null')
545 fd = dev_null.fileno()
554 raise SystemExit('failed to fork: '+str(e))
556 def do_vend_server(options, config_opts):
559 rfh, wfh = connect_to_vend(options, config_opts)
560 except (LATClientException, socket.error), e:
561 (exc_type, exc_value, exc_traceback) = sys.exc_info()
563 logging.error("Connection error: "+str(exc_type)+" "+str(e))
564 logging.info("Trying again in 5 seconds.")
569 run_forever(rfh, wfh, options, config_opts)
570 except VendingException:
571 logging.error("Connection died, trying again...")
572 logging.info("Trying again in 5 seconds.")
575 if __name__ == '__main__':
576 options, config_opts = set_stuff_up()
579 logging.warning('Starting Vend Server')
580 do_vend_server(options, config_opts)
581 logging.error('Vend Server finished unexpectedly, restarting')
582 except KeyboardInterrupt:
583 logging.info("Killed by signal, cleaning up")
584 clean_up_nicely(options, config_opts)
585 logging.warning("Vend Server stopped")
590 (exc_type, exc_value, exc_traceback) = sys.exc_info()
591 tb = format_tb(exc_traceback, 20)
594 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
595 logging.critical("Message: " + str(exc_value))
596 logging.critical("Traceback:")
598 for line in event.split('\n'):
599 logging.critical(' '+line)
600 logging.critical("This message should be considered a bug in the Vend Server.")
601 logging.critical("Please report this to someone who can fix it.")
603 logging.warning("Trying again anyway (might not help, but hey...)")