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 HorizScroll import HorizScroll
16 from random import random, seed
17 from Idler import TrainIdler,GrayIdler
19 from posix import geteuid
21 GREETING = 'UCC SNACKS'
28 class DispenseDatabaseException(Exception): pass
30 class DispenseDatabase:
31 def __init__(self, vending_machine, host, name, user, password):
32 self.vending_machine = vending_machine
33 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
34 self.db.query('LISTEN vend_requests')
36 def process_requests(self):
37 logging.debug('database processing')
38 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
40 outstanding = self.db.query(query).getresult()
41 except (pg.error,), db_err:
42 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
43 for (id, slot) in outstanding:
44 (worked, code, string) = self.vending_machine.vend(slot)
45 logging.debug (str((worked, code, string)))
47 query = 'SELECT vend_success(%s)'%id
48 self.db.query(query).getresult()
50 query = 'SELECT vend_failed(%s)'%id
51 self.db.query(query).getresult()
53 def handle_events(self):
54 notifier = self.db.getnotify()
55 while notifier is not None:
56 self.process_requests()
57 notify = self.db.getnotify()
59 def scroll_options(username, mk, welcome = False):
61 msg = [(center('WELCOME'), False, 0.8),
62 (center(username), False, 0.8)]
65 choices = ' '*10+'CHOICES: '
67 coke_machine = file('/home/other/coke/coke_contents')
68 cokes = coke_machine.readlines()
75 (slot_num, price, slot_name) = c.split(' ', 2)
76 if slot_name == 'dead': continue
77 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
79 choices += 'OR A SNACK. '
80 choices += '99 TO READ AGAIN. '
82 msg.append((choices, False, None))
87 info = pwd.getpwuid(uid)
89 logging.info('getting pin for uid %d: user not in password file'%uid)
91 if info.pw_dir == None: return False
92 pinfile = os.path.join(info.pw_dir, '.pin')
96 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
99 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
104 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
106 pinstr = f.readline()
108 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
109 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
113 def has_good_pin(uid):
114 return get_pin(uid) != None
116 def verify_user_pin(uid, pin):
117 if get_pin(uid) == pin:
118 info = pwd.getpwuid(uid)
119 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
122 logging.info('refused pin for uid %d'%(uid))
125 def door_open_mode(v):
126 logging.warning("Entering open door mode")
127 v.display("-FEED ME-")
133 if params == 1: # door closed
134 logging.warning('Leaving open door mode')
135 v.display("-YUM YUM!-")
141 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
142 choice = int(random()*len(messages))
143 msg = messages[choice]
144 left = range(len(msg))
145 for i in range(len(msg)):
146 if msg[i] == ' ': left.remove(i)
150 for i in range(0, len(msg)):
156 s += chr(int(random()*26)+ord('A'))
165 return ' '*((LEN-len(str))/2)+str
168 def __init__(self, vendie):
169 # Each element of scrolling_message should be a 3-tuple of
170 # ('message', True/False if it is to be repeated, time to display)
171 self.scrolling_message = []
173 self.next_update = None
175 def set_message(self, string):
176 self.scrolling_message = [(string, False, None)]
177 self.update_display(True)
179 def set_messages(self, strings):
180 self.scrolling_message = strings
181 self.update_display(True)
183 def update_display(self, forced = False):
184 if not forced and self.next_update != None and time() < self.next_update:
186 if len(self.scrolling_message) > 0:
187 if len(self.scrolling_message[0][0]) > 10:
188 (m, r, t) = self.scrolling_message[0]
190 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
197 del self.scrolling_message[0]
198 self.scrolling_message = a + self.scrolling_message
199 newmsg = self.scrolling_message[0]
200 if newmsg[2] != None:
201 self.next_update = time() + newmsg[2]
203 self.next_update = None
204 self.v.display(self.scrolling_message[0][0])
205 if self.scrolling_message[0][1]:
206 self.scrolling_message.append(self.scrolling_message[0])
207 del self.scrolling_message[0]
210 return len(self.scrolling_message) == 0
219 GrayIdler(v,one="*",zero="-"),
220 GrayIdler(v,one="/",zero="\\"),
221 GrayIdler(v,one="X",zero="O"),
223 idler = choose_idler()
227 idler = idlers[int(random()*len(idlers))]
234 def run_forever(rfh, wfh, options, cf):
235 v = VendingMachine(rfh, wfh)
236 logging.debug('PING is ' + str(v.ping()))
238 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
243 mk = MessageKeeper(v)
244 mk.set_message(GREETING)
245 time_to_autologout = None
248 last_timeout_refresh = None
254 except DispenseDatabaseException, e:
255 logging.error('Database error: '+str(e))
257 if time_to_autologout != None:
258 time_left = time_to_autologout - time()
259 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
260 mk.set_message('LOGOUT: '+str(int(time_left)))
261 last_timeout_refresh = int(time_left)
264 if time_to_autologout != None and time_to_autologout - time() <= 0:
265 time_to_autologout = None
269 mk.set_message(GREETING)
271 if time_to_autologout and not mk.done(): time_to_autologout = None
272 if cur_user == '' and time_to_autologout: time_to_autologout = None
273 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
275 time_to_autologout = time() + 15
277 if time_to_idle == None and cur_user == '':
278 time_to_idle = time() + 2
280 if time_to_idle != None and cur_user != '': time_to_idle = None
281 if time_to_idle is not None and time() > time_to_idle: idle_step()
287 e = v.next_event(0.05)
292 logging.debug('Got event: ' + repr(e))
298 mk.set_message(GREETING)
299 elif event == SWITCH:
300 # don't care right now.
304 # complicated key handling here:
305 if len(cur_user) < 5:
308 mk.set_message(GREETING)
310 cur_user += chr(key + ord('0'))
311 mk.set_message('UID: '+cur_user)
312 if len(cur_user) == 5:
314 if not has_good_pin(uid):
315 logging.info('user '+cur_user+' has a bad PIN')
317 #[(center('INVALID'), False, 0.7),
318 #(center('PIN'), False, 0.7),
319 #(center('SETUP'), False, 1.0),
320 #(GREETING, False, None)])
322 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
323 (GREETING, False, None)])
328 mk.set_message('PIN: ')
329 logging.info('need pin for user %s'%cur_user)
331 elif len(cur_pin) < PIN_LENGTH:
335 mk.set_message(GREETING)
338 mk.set_message('PIN: ')
340 cur_pin += chr(key + ord('0'))
341 mk.set_message('PIN: '+'X'*len(cur_pin))
342 if len(cur_pin) == PIN_LENGTH:
343 username = verify_user_pin(int(cur_user), int(cur_pin))
347 scroll_options(username, mk, True)
352 [(center('BAD PIN'), False, 1.0),
353 (center('SORRY'), False, 0.5),
354 (GREETING, False, None)])
358 elif len(cur_selection) == 0:
364 [(center('BYE!'), False, 1.5),
365 (GREETING, False, None)])
367 cur_selection += chr(key + ord('0'))
368 mk.set_message('SELECT: '+cur_selection)
369 time_to_autologout = None
370 elif len(cur_selection) == 1:
373 time_to_autologout = None
374 scroll_options(username, mk)
377 cur_selection += chr(key + ord('0'))
378 #make_selection(cur_selection)
379 # XXX this should move somewhere else:
380 if cur_selection == '55':
381 mk.set_message('OPENSESAME')
382 logging.info('dispensing a door for %s'%username)
384 ret = os.system('su - "%s" -c "dispense door"'%username)
386 ret = os.system('dispense door')
388 logging.info('door opened')
389 mk.set_message(center('DOOR OPEN'))
391 logging.warning('user %s tried to dispense a bad door'%username)
392 mk.set_message(center('BAD DOOR'))
394 elif cur_selection == '91':
396 elif cur_selection == '99':
397 scroll_options(username, mk)
400 elif cur_selection[1] == '8':
401 v.display('GOT COKE?')
402 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
404 v.display('HERES A '+cur_selection)
405 v.vend(cur_selection)
407 v.display('THANK YOU')
410 time_to_autologout = time() + 8
412 def connect_to_vend(options, cf):
413 # Open vending machine via LAT?
415 logging.info('Connecting to vending machine using LAT')
416 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
417 rfh, wfh = latclient.get_fh()
419 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
420 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
422 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
423 sock.connect((options.host, options.port))
424 rfh = sock.makefile('r')
425 wfh = sock.makefile('w')
430 from optparse import OptionParser
432 op = OptionParser(usage="%prog [OPTION]...")
433 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')
434 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
435 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
436 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
437 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
438 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
439 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
440 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
441 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
442 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
443 options, args = op.parse_args()
446 op.error('extra command line arguments: ' + ' '.join(args))
451 'DBServer': ('Database', 'Server'),
452 'DBName': ('Database', 'Name'),
453 'DBUser': ('VendingMachine', 'DBUser'),
454 'DBPassword': ('VendingMachine', 'DBPassword'),
456 'ServiceName': ('VendingMachine', 'ServiceName'),
457 'ServicePassword': ('VendingMachine', 'Password'),
459 'ServerName': ('DecServer', 'Name'),
460 'ConnectPassword': ('DecServer', 'ConnectPassword'),
461 'PrivPassword': ('DecServer', 'PrivPassword'),
464 class VendConfigFile:
465 def __init__(self, config_file, options):
467 cp = ConfigParser.ConfigParser()
470 for option in options:
471 section, name = options[option]
472 value = cp.get(section, name)
473 self.__dict__[option] = value
475 except ConfigParser.Error, e:
476 raise SystemExit("Error reading config file "+config_file+": " + str(e))
478 def create_pid_file(name):
480 pid_file = file(name, 'w')
481 pid_file.write('%d\n'%os.getpid())
484 logging.warning('unable to write to pid file '+name+': '+str(e))
487 def do_nothing(signum, stack):
488 signal.signal(signum, do_nothing)
489 def stop_server(signum, stack): raise KeyboardInterrupt
490 signal.signal(signal.SIGHUP, do_nothing)
491 signal.signal(signal.SIGTERM, stop_server)
492 signal.signal(signal.SIGINT, stop_server)
494 options = parse_args()
495 config_opts = VendConfigFile(options.config_file, config_options)
496 if options.daemon: become_daemon()
497 set_up_logging(options)
498 if options.pid_file != '': create_pid_file(options.pid_file)
500 return options, config_opts
502 def clean_up_nicely(options, config_opts):
503 if options.pid_file != '':
505 os.unlink(options.pid_file)
506 logging.debug('Removed pid file '+options.pid_file)
507 except OSError: pass # if we can't delete it, meh
509 def set_up_logging(options):
510 logger = logging.getLogger()
512 if not options.daemon:
513 stderr_logger = logging.StreamHandler(sys.stderr)
514 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
515 logger.addHandler(stderr_logger)
517 if options.log_file != '':
519 file_logger = logging.FileHandler(options.log_file)
520 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
521 logger.addHandler(file_logger)
523 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
525 if options.syslog != None:
526 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
527 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
528 logger.addHandler(sys_logger)
531 logger.setLevel(logging.WARNING)
532 elif options.verbose:
533 logger.setLevel(logging.DEBUG)
535 logger.setLevel(logging.INFO)
538 dev_null = file('/dev/null')
539 fd = dev_null.fileno()
548 raise SystemExit('failed to fork: '+str(e))
550 def do_vend_server(options, config_opts):
553 rfh, wfh = connect_to_vend(options, config_opts)
554 except (LATClientException, socket.error), e:
555 (exc_type, exc_value, exc_traceback) = sys.exc_info()
557 logging.error("Connection error: "+str(exc_type)+" "+str(e))
558 logging.info("Trying again in 5 seconds.")
563 run_forever(rfh, wfh, options, config_opts)
564 except VendingException:
565 logging.error("Connection died, trying again...")
566 logging.info("Trying again in 5 seconds.")
569 if __name__ == '__main__':
570 options, config_opts = set_stuff_up()
573 logging.warning('Starting Vend Server')
574 do_vend_server(options, config_opts)
575 logging.error('Vend Server finished unexpectedly, restarting')
576 except KeyboardInterrupt:
577 logging.info("Killed by signal, cleaning up")
578 clean_up_nicely(options, config_opts)
579 logging.warning("Vend Server stopped")
584 (exc_type, exc_value, exc_traceback) = sys.exc_info()
585 tb = format_tb(exc_traceback, 20)
588 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
589 logging.critical("Message: " + str(exc_value))
590 logging.critical("Traceback:")
592 for line in event.split('\n'):
593 logging.critical(' '+line)
594 logging.critical("This message should be considered a bug in the Vend Server.")
595 logging.critical("Please report this to someone who can fix it.")
597 logging.warning("Trying again anyway (might not help, but hey...)")