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
212 def run_forever(rfh, wfh, options, cf):
213 v = VendingMachine(rfh, wfh)
214 logging.debug('PING is ' + str(v.ping()))
216 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
221 mk = MessageKeeper(v)
222 mk.set_message(GREETING)
223 time_to_autologout = None
224 #idler = TrainIdler(v)
225 #idler = GrayIdler(v)
226 idler = GrayIdler(v,one="*",zero="-")
228 last_timeout_refresh = None
234 except DispenseDatabaseException, e:
235 logging.error('Database error: '+str(e))
237 if time_to_autologout != None:
238 time_left = time_to_autologout - time()
239 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
240 mk.set_message('LOGOUT: '+str(int(time_left)))
241 last_timeout_refresh = int(time_left)
244 if time_to_autologout != None and time_to_autologout - time() <= 0:
245 time_to_autologout = None
249 mk.set_message(GREETING)
251 if time_to_autologout and not mk.done(): time_to_autologout = None
252 if cur_user == '' and time_to_autologout: time_to_autologout = None
253 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
255 time_to_autologout = time() + 15
257 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
258 if time_to_idle != None and cur_user != '': time_to_idle = None
259 if time_to_idle is not None and time() > time_to_idle: idler.next()
265 e = v.next_event(0.1)
270 logging.debug('Got event: ' + repr(e))
276 mk.set_message(GREETING)
277 elif event == SWITCH:
278 # don't care right now.
282 # complicated key handling here:
283 if len(cur_user) < 5:
286 mk.set_message(GREETING)
288 cur_user += chr(key + ord('0'))
289 mk.set_message('UID: '+cur_user)
290 if len(cur_user) == 5:
292 if not has_good_pin(uid):
293 logging.info('user '+cur_user+' has a bad PIN')
295 #[(center('INVALID'), False, 0.7),
296 #(center('PIN'), False, 0.7),
297 #(center('SETUP'), False, 1.0),
298 #(GREETING, False, None)])
300 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
301 (GREETING, False, None)])
306 mk.set_message('PIN: ')
307 logging.info('need pin for user %s'%cur_user)
309 elif len(cur_pin) < PIN_LENGTH:
313 mk.set_message(GREETING)
316 mk.set_message('PIN: ')
318 cur_pin += chr(key + ord('0'))
319 mk.set_message('PIN: '+'X'*len(cur_pin))
320 if len(cur_pin) == PIN_LENGTH:
321 username = verify_user_pin(int(cur_user), int(cur_pin))
325 scroll_options(username, mk, True)
330 [(center('BAD PIN'), False, 1.0),
331 (center('SORRY'), False, 0.5),
332 (GREETING, False, None)])
336 elif len(cur_selection) == 0:
342 [(center('BYE!'), False, 1.5),
343 (GREETING, False, None)])
345 cur_selection += chr(key + ord('0'))
346 mk.set_message('SELECT: '+cur_selection)
347 time_to_autologout = None
348 elif len(cur_selection) == 1:
351 time_to_autologout = None
352 scroll_options(username, mk)
355 cur_selection += chr(key + ord('0'))
356 #make_selection(cur_selection)
357 # XXX this should move somewhere else:
358 if cur_selection == '55':
359 mk.set_message('OPENSESAME')
360 logging.info('dispensing a door for %s'%username)
362 ret = os.system('su - "%s" -c "dispense door"'%username)
364 ret = os.system('dispense door')
366 logging.info('door opened')
367 mk.set_message(center('DOOR OPEN'))
369 logging.warning('user %s tried to dispense a bad door'%username)
370 mk.set_message(center('BAD DOOR'))
372 elif cur_selection == '91':
374 elif cur_selection == '99':
375 scroll_options(username, mk)
378 elif cur_selection[1] == '8':
379 v.display('GOT COKE?')
380 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
382 v.display('HERES A '+cur_selection)
383 v.vend(cur_selection)
385 v.display('THANK YOU')
388 time_to_autologout = time() + 8
390 def connect_to_vend(options, cf):
391 # Open vending machine via LAT?
393 logging.info('Connecting to vending machine using LAT')
394 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
395 rfh, wfh = latclient.get_fh()
397 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
398 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
400 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
401 sock.connect((options.host, options.port))
402 rfh = sock.makefile('r')
403 wfh = sock.makefile('w')
408 from optparse import OptionParser
410 op = OptionParser(usage="%prog [OPTION]...")
411 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')
412 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
413 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
414 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
415 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
416 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
417 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
418 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
419 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
420 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
421 options, args = op.parse_args()
424 op.error('extra command line arguments: ' + ' '.join(args))
429 'DBServer': ('Database', 'Server'),
430 'DBName': ('Database', 'Name'),
431 'DBUser': ('VendingMachine', 'DBUser'),
432 'DBPassword': ('VendingMachine', 'DBPassword'),
434 'ServiceName': ('VendingMachine', 'ServiceName'),
435 'ServicePassword': ('VendingMachine', 'Password'),
437 'ServerName': ('DecServer', 'Name'),
438 'ConnectPassword': ('DecServer', 'ConnectPassword'),
439 'PrivPassword': ('DecServer', 'PrivPassword'),
442 class VendConfigFile:
443 def __init__(self, config_file, options):
445 cp = ConfigParser.ConfigParser()
448 for option in options:
449 section, name = options[option]
450 value = cp.get(section, name)
451 self.__dict__[option] = value
453 except ConfigParser.Error, e:
454 raise SystemExit("Error reading config file "+config_file+": " + str(e))
456 def create_pid_file(name):
458 pid_file = file(name, 'w')
459 pid_file.write('%d\n'%os.getpid())
462 logging.warning('unable to write to pid file '+name+': '+str(e))
465 def do_nothing(signum, stack):
466 signal.signal(signum, do_nothing)
467 def stop_server(signum, stack): raise KeyboardInterrupt
468 signal.signal(signal.SIGHUP, do_nothing)
469 signal.signal(signal.SIGTERM, stop_server)
470 signal.signal(signal.SIGINT, stop_server)
472 options = parse_args()
473 config_opts = VendConfigFile(options.config_file, config_options)
474 if options.daemon: become_daemon()
475 set_up_logging(options)
476 if options.pid_file != '': create_pid_file(options.pid_file)
478 return options, config_opts
480 def clean_up_nicely(options, config_opts):
481 if options.pid_file != '':
483 os.unlink(options.pid_file)
484 logging.debug('Removed pid file '+options.pid_file)
485 except OSError: pass # if we can't delete it, meh
487 def set_up_logging(options):
488 logger = logging.getLogger()
490 if not options.daemon:
491 stderr_logger = logging.StreamHandler(sys.stderr)
492 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
493 logger.addHandler(stderr_logger)
495 if options.log_file != '':
497 file_logger = logging.FileHandler(options.log_file)
498 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
499 logger.addHandler(file_logger)
501 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
503 if options.syslog != None:
504 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
505 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
506 logger.addHandler(sys_logger)
509 logger.setLevel(logging.WARNING)
510 elif options.verbose:
511 logger.setLevel(logging.DEBUG)
513 logger.setLevel(logging.INFO)
516 dev_null = file('/dev/null')
517 fd = dev_null.fileno()
526 raise SystemExit('failed to fork: '+str(e))
528 def do_vend_server(options, config_opts):
531 rfh, wfh = connect_to_vend(options, config_opts)
532 except (LATClientException, socket.error), e:
533 (exc_type, exc_value, exc_traceback) = sys.exc_info()
535 logging.error("Connection error: "+str(exc_type)+" "+str(e))
536 logging.info("Trying again in 5 seconds.")
541 run_forever(rfh, wfh, options, config_opts)
542 except VendingException:
543 logging.error("Connection died, trying again...")
544 logging.info("Trying again in 5 seconds.")
547 if __name__ == '__main__':
548 options, config_opts = set_stuff_up()
551 logging.warning('Starting Vend Server')
552 do_vend_server(options, config_opts)
553 logging.error('Vend Server finished unexpectedly, restarting')
554 except KeyboardInterrupt:
555 logging.info("Killed by signal, cleaning up")
556 clean_up_nicely(options, config_opts)
557 logging.warning("Vend Server stopped")
560 (exc_type, exc_value, exc_traceback) = sys.exc_info()
561 tb = format_tb(exc_traceback, 20)
564 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
565 logging.critical("Message: " + str(exc_value))
566 logging.critical("Traceback:")
568 for line in event.split('\n'):
569 logging.critical(' '+line)
570 logging.critical("This message should be considered a bug in the Vend Server.")
571 logging.critical("Please report this to someone who can fix it.")
573 logging.warning("Trying again anyway (might not help, but hey...)")