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)
90 if info.pw_dir == None: return False
91 pinfile = os.path.join(info.pw_dir, '.pin')
102 pinstr = f.readline()
104 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
108 def has_good_pin(uid):
109 return get_pin(uid) != None
111 def verify_user_pin(uid, pin):
112 if get_pin(uid) == pin:
113 info = pwd.getpwuid(uid)
118 def door_open_mode(v):
119 logging.warning("Entering open door mode")
120 v.display("-FEED ME-")
126 if params == 1: # door closed
127 logging.warning('Leaving open door mode')
128 v.display("-YUM YUM!-")
134 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
135 choice = int(random()*len(messages))
136 msg = messages[choice]
137 left = range(len(msg))
138 for i in range(len(msg)):
139 if msg[i] == ' ': left.remove(i)
143 for i in range(0, len(msg)):
149 s += chr(int(random()*26)+ord('A'))
158 return ' '*((LEN-len(str))/2)+str
161 def __init__(self, vendie):
162 # Each element of scrolling_message should be a 3-tuple of
163 # ('message', True/False if it is to be repeated, time to display)
164 self.scrolling_message = []
166 self.next_update = None
168 def set_message(self, string):
169 self.scrolling_message = [(string, False, None)]
170 self.update_display(True)
172 def set_messages(self, strings):
173 self.scrolling_message = strings
174 self.update_display(True)
176 def update_display(self, forced = False):
177 if not forced and self.next_update != None and time() < self.next_update:
179 if len(self.scrolling_message) > 0:
180 if len(self.scrolling_message[0][0]) > 10:
181 (m, r, t) = self.scrolling_message[0]
183 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
190 del self.scrolling_message[0]
191 self.scrolling_message = a + self.scrolling_message
192 newmsg = self.scrolling_message[0]
193 if newmsg[2] != None:
194 self.next_update = time() + newmsg[2]
196 self.next_update = None
197 self.v.display(self.scrolling_message[0][0])
198 if self.scrolling_message[0][1]:
199 self.scrolling_message.append(self.scrolling_message[0])
200 del self.scrolling_message[0]
203 return len(self.scrolling_message) == 0
205 def run_forever(rfh, wfh, options, cf):
206 v = VendingMachine(rfh, wfh)
207 logging.debug('PING is' + str(v.ping()))
209 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
214 mk = MessageKeeper(v)
215 mk.set_message(GREETING)
216 time_to_autologout = None
217 #idler = TrainIdler(v)
218 #idler = GrayIdler(v)
219 idler = GrayIdler(v,one="*",zero="-")
221 last_timeout_refresh = None
227 except DispenseDatabaseException, e:
228 logging.error('Database error: '+str(e))
230 if time_to_autologout != None:
231 time_left = time_to_autologout - time()
232 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
233 mk.set_message('LOGOUT: '+str(int(time_left)))
234 last_timeout_refresh = int(time_left)
237 if time_to_autologout != None and time_to_autologout - time() <= 0:
238 time_to_autologout = None
242 mk.set_message(GREETING)
244 if time_to_autologout and not mk.done(): time_to_autologout = None
245 if cur_user == '' and time_to_autologout: time_to_autologout = None
246 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
248 time_to_autologout = time() + 15
250 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
251 if time_to_idle != None and cur_user != '': time_to_idle = None
252 if time_to_idle is not None and time() > time_to_idle: idler.next()
258 e = v.next_event(0.1)
263 logging.debug('Got event: ' + repr(e))
269 mk.set_message(GREETING)
270 elif event == SWITCH:
271 # don't care right now.
275 # complicated key handling here:
276 if len(cur_user) < 5:
279 mk.set_message(GREETING)
281 cur_user += chr(key + ord('0'))
282 mk.set_message('UID: '+cur_user)
283 if len(cur_user) == 5:
285 if not has_good_pin(uid):
287 #[(center('INVALID'), False, 0.7),
288 #(center('PIN'), False, 0.7),
289 #(center('SETUP'), False, 1.0),
290 #(GREETING, False, None)])
292 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
293 (GREETING, False, None)])
298 mk.set_message('PIN: ')
300 elif len(cur_pin) < PIN_LENGTH:
304 mk.set_message(GREETING)
307 mk.set_message('PIN: ')
309 cur_pin += chr(key + ord('0'))
310 mk.set_message('PIN: '+'X'*len(cur_pin))
311 if len(cur_pin) == PIN_LENGTH:
312 username = verify_user_pin(int(cur_user), int(cur_pin))
316 scroll_options(username, mk, True)
321 [(center('BAD PIN'), False, 1.0),
322 (center('SORRY'), False, 0.5),
323 (GREETING, False, None)])
327 elif len(cur_selection) == 0:
333 [(center('BYE!'), False, 1.5),
334 (GREETING, False, None)])
336 cur_selection += chr(key + ord('0'))
337 mk.set_message('SELECT: '+cur_selection)
338 time_to_autologout = None
339 elif len(cur_selection) == 1:
342 time_to_autologout = None
343 scroll_options(username, mk)
346 cur_selection += chr(key + ord('0'))
347 #make_selection(cur_selection)
348 # XXX this should move somewhere else:
349 if cur_selection == '55':
350 mk.set_message('OPENSESAME')
352 ret = os.system('su - "%s" -c "dispense door"'%username)
354 ret = os.system('dispense door')
356 mk.set_message(center('DOOR OPEN'))
358 mk.set_message(center('BAD DOOR'))
360 elif cur_selection == '91':
362 elif cur_selection == '99':
363 scroll_options(username, mk)
366 elif cur_selection[1] == '8':
367 v.display('GOT COKE?')
368 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
370 v.display('HERES A '+cur_selection)
371 v.vend(cur_selection)
373 v.display('THANK YOU')
376 time_to_autologout = time() + 8
378 def connect_to_vend(options, cf):
379 # Open vending machine via LAT?
381 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
382 rfh, wfh = latclient.get_fh()
384 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
386 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
387 sock.connect((options.host, options.port))
388 rfh = sock.makefile('r')
389 wfh = sock.makefile('w')
394 from optparse import OptionParser
396 op = OptionParser(usage="%prog [OPTION]...")
397 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')
398 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
399 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
400 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
401 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
402 op.add_option('-s', '--syslog', dest='syslog', action='store_true', default=False, help='log output to syslog')
403 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
404 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
405 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
406 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
407 options, args = op.parse_args()
410 op.error('extra command line arguments: ' + ' '.join(args))
415 'DBServer': ('Database', 'Server'),
416 'DBName': ('Database', 'Name'),
417 'DBUser': ('VendingMachine', 'DBUser'),
418 'DBPassword': ('VendingMachine', 'DBPassword'),
420 'ServiceName': ('VendingMachine', 'ServiceName'),
421 'ServicePassword': ('VendingMachine', 'Password'),
423 'ServerName': ('DecServer', 'Name'),
424 'ConnectPassword': ('DecServer', 'ConnectPassword'),
425 'PrivPassword': ('DecServer', 'PrivPassword'),
428 class VendConfigFile:
429 def __init__(self, config_file, options):
431 cp = ConfigParser.ConfigParser()
434 for option in options:
435 section, name = options[option]
436 value = cp.get(section, name)
437 self.__dict__[option] = value
439 except ConfigParser.Error, e:
440 logging.critical("Error reading config file "+config_file+": " + str(e))
441 logging.critical("Bailing out")
444 def create_pid_file(name):
446 pid_file = file(name, 'w')
447 pid_file.write('%d\n'%os.getpid())
450 logging.warning('unable to write to pid file '+name+': '+str(e))
453 def do_nothing(signum, stack):
454 signal.signal(signum, do_nothing)
455 def stop_server(signum, stack): raise KeyboardInterrupt
456 signal.signal(signal.SIGHUP, do_nothing)
457 signal.signal(signal.SIGTERM, stop_server)
458 signal.signal(signal.SIGINT, stop_server)
460 options = parse_args()
461 set_up_logging(options)
462 config_opts = VendConfigFile(options.config_file, config_options)
463 if options.daemon: become_daemon()
464 if options.pid_file != '': create_pid_file(options.pid_file)
466 return options, config_opts
468 def clean_up_nicely(options, config_opts):
469 if options.pid_file != '':
471 os.unlink(options.pid_file)
472 logging.debug('Removed pid file '+options.pid_file)
473 except OSError: pass # if we can't delete it, meh
475 def set_up_logging(options):
476 logger = logging.getLogger()
478 stderr_logger = logging.StreamHandler(sys.stderr)
479 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
480 logger.addHandler(stderr_logger)
482 if options.log_file != '':
484 file_logger = logging.FileHandler(options.log_file)
485 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
486 logger.addHandler(file_logger)
488 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
491 sys_logger = logging.handlers.SysLogHandler('/dev/log', 'daemon')
492 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
493 logger.addHandler(sys_logger)
496 logger.setLevel(logging.WARNING)
497 elif options.verbose:
498 logger.setLevel(logging.DEBUG)
500 logger.setLevel(logging.INFO)
503 dev_null = file('/dev/null')
504 fd = dev_null.fileno()
511 def do_vend_server(options, config_opts):
514 rfh, wfh = connect_to_vend(options, config_opts)
515 except (LATClientException, socket.error), e:
516 (exc_type, exc_value, exc_traceback) = sys.exc_info()
518 logging.error("Connection error: "+str(exc_type)+" "+str(e))
519 logging.info("Trying again in 5 seconds.")
524 run_forever(rfh, wfh, options, config_opts)
525 except VendingException:
526 logging.error("Connection died, trying again...")
527 logging.info("Trying again in 5 seconds.")
530 if __name__ == '__main__':
531 options, config_opts = set_stuff_up()
534 logging.warning('Starting Vend Server')
535 do_vend_server(options, config_opts)
536 logging.error('Vend Server finished unexpectedly, restarting')
537 except KeyboardInterrupt:
538 logging.info("Killed by signal, cleaning up")
539 clean_up_nicely(options, config_opts)
540 logging.warning("Vend Server stopped")
543 (exc_type, exc_value, exc_traceback) = sys.exc_info()
544 tb = format_tb(exc_traceback, 20)
547 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
548 logging.critical("Message: " + str(exc_value))
549 logging.critical("Traceback:")
551 for line in event.split('\n'):
552 logging.critical(' '+line)
553 logging.critical("This message should be considered a bug in the Vend Server.")
554 logging.critical("Please report this to someone who can fix it.")
556 logging.warning("Trying again anyway (might not help, but hey...)")