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"),
222 GrayIdler(v,one="*",zero="-",reorder=1),
223 GrayIdler(v,one="/",zero="\\",reorder=1),
224 GrayIdler(v,one="X",zero="O",reorder=1),
226 idler = choose_idler()
230 idler = idlers[int(random()*len(idlers))]
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() + 30
283 if time_to_idle != 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() > 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...)")