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
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
192 StringIdler(v, text="Kill 'em all", repeat=False),
193 StringIdler(v, text=CREDITS),
194 StringIdler(v, text=str(math.pi) + " "),
195 StringIdler(v, text=str(math.e) + " "),
201 GrayIdler(v,one="*",zero="-"),
202 GrayIdler(v,one="/",zero="\\"),
203 GrayIdler(v,one="X",zero="O"),
204 GrayIdler(v,one="*",zero="-",reorder=1),
205 GrayIdler(v,one="/",zero="\\",reorder=1),
206 GrayIdler(v,one="X",zero="O",reorder=1),
210 idler = choose_idler()
217 iiindex = idlers.index(idler)
221 move = int(random()*len(idlers)) + 1
224 idler = idlers[( (iiindex + 1) % iilen)]
225 move = move - idler.affinity()
235 def run_forever(rfh, wfh, options, cf):
236 v = VendingMachine(rfh, wfh)
237 logging.debug('PING is ' + str(v.ping()))
239 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
244 mk = MessageKeeper(v)
245 mk.set_message(GREETING)
246 time_to_autologout = None
249 last_timeout_refresh = None
255 except DispenseDatabaseException, e:
256 logging.error('Database error: '+str(e))
258 if time_to_autologout != None:
259 time_left = time_to_autologout - time()
260 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
261 mk.set_message('LOGOUT: '+str(int(time_left)))
262 last_timeout_refresh = int(time_left)
265 if time_to_autologout != None and time_to_autologout - time() <= 0:
266 time_to_autologout = None
270 mk.set_message(GREETING)
272 if time_to_autologout and not mk.done(): time_to_autologout = None
273 if cur_user == '' and time_to_autologout: time_to_autologout = None
274 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
276 time_to_autologout = time() + 15
278 if time_to_idle == None and cur_user == '':
279 time_to_idle = time() + 5
281 if time_to_idle is not None and cur_user != '': time_to_idle = None
282 if time_to_idle is not None and time() > time_to_idle: idle_step()
283 if time_to_idle is not None and time() > time_to_idle + 300:
284 time_to_idle = time()
291 e = v.next_event(0.05)
296 logging.debug('Got event: ' + repr(e))
302 mk.set_message(GREETING)
303 elif event == SWITCH:
304 # don't care right now.
308 # complicated key handling here:
309 if len(cur_user) < 5:
312 mk.set_message(GREETING)
314 cur_user += chr(key + ord('0'))
315 mk.set_message('UID: '+cur_user)
316 if len(cur_user) == 5:
318 if not has_good_pin(uid):
319 logging.info('user '+cur_user+' has a bad PIN')
321 #[(center('INVALID'), False, 0.7),
322 #(center('PIN'), False, 0.7),
323 #(center('SETUP'), False, 1.0),
324 #(GREETING, False, None)])
326 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
327 (GREETING, False, None)])
332 mk.set_message('PIN: ')
333 logging.info('need pin for user %s'%cur_user)
335 elif len(cur_pin) < PIN_LENGTH:
339 mk.set_message(GREETING)
342 mk.set_message('PIN: ')
344 cur_pin += chr(key + ord('0'))
345 mk.set_message('PIN: '+'X'*len(cur_pin))
346 if len(cur_pin) == PIN_LENGTH:
347 username = verify_user_pin(int(cur_user), int(cur_pin))
351 scroll_options(username, mk, True)
356 [(center('BAD PIN'), False, 1.0),
357 (center('SORRY'), False, 0.5),
358 (GREETING, False, None)])
362 elif len(cur_selection) == 0:
368 [(center('BYE!'), False, 1.5),
369 (GREETING, False, None)])
371 cur_selection += chr(key + ord('0'))
372 mk.set_message('SELECT: '+cur_selection)
373 time_to_autologout = None
374 elif len(cur_selection) == 1:
377 time_to_autologout = None
378 scroll_options(username, mk)
381 cur_selection += chr(key + ord('0'))
382 #make_selection(cur_selection)
383 # XXX this should move somewhere else:
384 if cur_selection == '55':
385 mk.set_message('OPENSESAME')
386 logging.info('dispensing a door for %s'%username)
388 ret = os.system('su - "%s" -c "dispense door"'%username)
390 ret = os.system('dispense door')
392 logging.info('door opened')
393 mk.set_message(center('DOOR OPEN'))
395 logging.warning('user %s tried to dispense a bad door'%username)
396 mk.set_message(center('BAD DOOR'))
398 elif cur_selection == '91':
400 elif cur_selection == '99':
401 scroll_options(username, mk)
404 elif cur_selection[1] == '8':
405 v.display('GOT COKE?')
406 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
408 v.display('HERES A '+cur_selection)
409 v.vend(cur_selection)
411 v.display('THANK YOU')
414 time_to_autologout = time() + 8
416 def connect_to_vend(options, cf):
417 # Open vending machine via LAT?
419 logging.info('Connecting to vending machine using LAT')
420 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
421 rfh, wfh = latclient.get_fh()
423 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
424 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
426 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
427 sock.connect((options.host, options.port))
428 rfh = sock.makefile('r')
429 wfh = sock.makefile('w')
434 from optparse import OptionParser
436 op = OptionParser(usage="%prog [OPTION]...")
437 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')
438 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
439 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
440 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
441 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
442 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
443 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
444 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
445 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
446 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
447 options, args = op.parse_args()
450 op.error('extra command line arguments: ' + ' '.join(args))
455 'DBServer': ('Database', 'Server'),
456 'DBName': ('Database', 'Name'),
457 'DBUser': ('VendingMachine', 'DBUser'),
458 'DBPassword': ('VendingMachine', 'DBPassword'),
460 'ServiceName': ('VendingMachine', 'ServiceName'),
461 'ServicePassword': ('VendingMachine', 'Password'),
463 'ServerName': ('DecServer', 'Name'),
464 'ConnectPassword': ('DecServer', 'ConnectPassword'),
465 'PrivPassword': ('DecServer', 'PrivPassword'),
468 class VendConfigFile:
469 def __init__(self, config_file, options):
471 cp = ConfigParser.ConfigParser()
474 for option in options:
475 section, name = options[option]
476 value = cp.get(section, name)
477 self.__dict__[option] = value
479 except ConfigParser.Error, e:
480 raise SystemExit("Error reading config file "+config_file+": " + str(e))
482 def create_pid_file(name):
484 pid_file = file(name, 'w')
485 pid_file.write('%d\n'%os.getpid())
488 logging.warning('unable to write to pid file '+name+': '+str(e))
491 def do_nothing(signum, stack):
492 signal.signal(signum, do_nothing)
493 def stop_server(signum, stack): raise KeyboardInterrupt
494 signal.signal(signal.SIGHUP, do_nothing)
495 signal.signal(signal.SIGTERM, stop_server)
496 signal.signal(signal.SIGINT, stop_server)
498 options = parse_args()
499 config_opts = VendConfigFile(options.config_file, config_options)
500 if options.daemon: become_daemon()
501 set_up_logging(options)
502 if options.pid_file != '': create_pid_file(options.pid_file)
504 return options, config_opts
506 def clean_up_nicely(options, config_opts):
507 if options.pid_file != '':
509 os.unlink(options.pid_file)
510 logging.debug('Removed pid file '+options.pid_file)
511 except OSError: pass # if we can't delete it, meh
513 def set_up_logging(options):
514 logger = logging.getLogger()
516 if not options.daemon:
517 stderr_logger = logging.StreamHandler(sys.stderr)
518 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
519 logger.addHandler(stderr_logger)
521 if options.log_file != '':
523 file_logger = logging.FileHandler(options.log_file)
524 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
525 logger.addHandler(file_logger)
527 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
529 if options.syslog != None:
530 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
531 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
532 logger.addHandler(sys_logger)
535 logger.setLevel(logging.WARNING)
536 elif options.verbose:
537 logger.setLevel(logging.DEBUG)
539 logger.setLevel(logging.INFO)
542 dev_null = file('/dev/null')
543 fd = dev_null.fileno()
552 raise SystemExit('failed to fork: '+str(e))
554 def do_vend_server(options, config_opts):
557 rfh, wfh = connect_to_vend(options, config_opts)
558 except (LATClientException, socket.error), e:
559 (exc_type, exc_value, exc_traceback) = sys.exc_info()
561 logging.error("Connection error: "+str(exc_type)+" "+str(e))
562 logging.info("Trying again in 5 seconds.")
567 run_forever(rfh, wfh, options, config_opts)
568 except VendingException:
569 logging.error("Connection died, trying again...")
570 logging.info("Trying again in 5 seconds.")
573 if __name__ == '__main__':
574 options, config_opts = set_stuff_up()
577 logging.warning('Starting Vend Server')
578 do_vend_server(options, config_opts)
579 logging.error('Vend Server finished unexpectedly, restarting')
580 except KeyboardInterrupt:
581 logging.info("Killed by signal, cleaning up")
582 clean_up_nicely(options, config_opts)
583 logging.warning("Vend Server stopped")
588 (exc_type, exc_value, exc_traceback) = sys.exc_info()
589 tb = format_tb(exc_traceback, 20)
592 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
593 logging.critical("Message: " + str(exc_value))
594 logging.critical("Traceback:")
596 for line in event.split('\n'):
597 logging.critical(' '+line)
598 logging.critical("This message should be considered a bug in the Vend Server.")
599 logging.critical("Please report this to someone who can fix it.")
601 logging.warning("Trying again anyway (might not help, but hey...)")