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', action='store_true', default=False, help='log output to syslog')
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 logging.critical("Error reading config file "+config_file+": " + str(e))
455 logging.critical("Bailing out")
458 def create_pid_file(name):
460 pid_file = file(name, 'w')
461 pid_file.write('%d\n'%os.getpid())
464 logging.warning('unable to write to pid file '+name+': '+str(e))
467 def do_nothing(signum, stack):
468 signal.signal(signum, do_nothing)
469 def stop_server(signum, stack): raise KeyboardInterrupt
470 signal.signal(signal.SIGHUP, do_nothing)
471 signal.signal(signal.SIGTERM, stop_server)
472 signal.signal(signal.SIGINT, stop_server)
474 options = parse_args()
475 set_up_logging(options)
476 config_opts = VendConfigFile(options.config_file, config_options)
477 if options.daemon: become_daemon()
478 if options.pid_file != '': create_pid_file(options.pid_file)
480 return options, config_opts
482 def clean_up_nicely(options, config_opts):
483 if options.pid_file != '':
485 os.unlink(options.pid_file)
486 logging.debug('Removed pid file '+options.pid_file)
487 except OSError: pass # if we can't delete it, meh
489 def set_up_logging(options):
490 logger = logging.getLogger()
492 stderr_logger = logging.StreamHandler(sys.stderr)
493 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
494 logger.addHandler(stderr_logger)
496 if options.log_file != '':
498 file_logger = logging.FileHandler(options.log_file)
499 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
500 logger.addHandler(file_logger)
502 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
505 sys_logger = logging.handlers.SysLogHandler('/dev/log', 'daemon')
506 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
507 logger.addHandler(sys_logger)
510 logger.setLevel(logging.WARNING)
511 elif options.verbose:
512 logger.setLevel(logging.DEBUG)
514 logger.setLevel(logging.INFO)
517 dev_null = file('/dev/null')
518 fd = dev_null.fileno()
525 def do_vend_server(options, config_opts):
528 rfh, wfh = connect_to_vend(options, config_opts)
529 except (LATClientException, socket.error), e:
530 (exc_type, exc_value, exc_traceback) = sys.exc_info()
532 logging.error("Connection error: "+str(exc_type)+" "+str(e))
533 logging.info("Trying again in 5 seconds.")
538 run_forever(rfh, wfh, options, config_opts)
539 except VendingException:
540 logging.error("Connection died, trying again...")
541 logging.info("Trying again in 5 seconds.")
544 if __name__ == '__main__':
545 options, config_opts = set_stuff_up()
548 logging.warning('Starting Vend Server')
549 do_vend_server(options, config_opts)
550 logging.error('Vend Server finished unexpectedly, restarting')
551 except KeyboardInterrupt:
552 logging.info("Killed by signal, cleaning up")
553 clean_up_nicely(options, config_opts)
554 logging.warning("Vend Server stopped")
557 (exc_type, exc_value, exc_traceback) = sys.exc_info()
558 tb = format_tb(exc_traceback, 20)
561 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
562 logging.critical("Message: " + str(exc_value))
563 logging.critical("Traceback:")
565 for line in event.split('\n'):
566 logging.critical(' '+line)
567 logging.critical("This message should be considered a bug in the Vend Server.")
568 logging.critical("Please report this to someone who can fix it.")
570 logging.warning("Trying again anyway (might not help, but hey...)")