7 import sys, os, string, re, pwd, signal
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 DispenseDatabase:
29 def __init__(self, vending_machine, host, name, user, password):
30 self.vending_machine = vending_machine
31 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
32 self.db.query('LISTEN vend_requests')
34 def process_requests(self):
36 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
38 outstanding = self.db.query(query).getresult()
39 except (pg.error,), db_err:
40 sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
42 for (id, slot) in outstanding:
43 (worked, code, string) = self.vending_machine.vend(slot)
44 print (worked, code, string)
46 query = 'SELECT vend_success(%s)'%id
47 self.db.query(query).getresult()
49 query = 'SELECT vend_failed(%s)'%id
50 self.db.query(query).getresult()
52 def handle_events(self):
53 notifier = self.db.getnotify()
54 while notifier is not None:
55 self.process_requests()
56 notify = self.db.getnotify()
58 def scroll_options(username, mk, welcome = False):
60 msg = [(center('WELCOME'), False, 0.8),
61 (center(username), False, 0.8)]
64 choices = ' '*10+'CHOICES: '
66 coke_machine = file('/home/other/coke/coke_contents')
67 cokes = coke_machine.readlines()
74 (slot_num, price, slot_name) = c.split(' ', 2)
75 if slot_name == 'dead': continue
76 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
78 choices += 'OR A SNACK. '
79 choices += '99 TO READ AGAIN. '
81 msg.append((choices, False, None))
86 info = pwd.getpwuid(uid)
89 if info.pw_dir == None: return False
90 pinfile = os.path.join(info.pw_dir, '.pin')
101 pinstr = f.readline()
103 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
107 def has_good_pin(uid):
108 return get_pin(uid) != None
110 def verify_user_pin(uid, pin):
111 if get_pin(uid) == pin:
112 info = pwd.getpwuid(uid)
117 def door_open_mode(v):
118 print "Entering open door mode"
119 v.display("-FEED ME-")
125 if params == 1: # door closed
126 v.display("-YUM YUM!-")
132 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
133 choice = int(random()*len(messages))
134 msg = messages[choice]
135 left = range(len(msg))
136 for i in range(len(msg)):
137 if msg[i] == ' ': left.remove(i)
141 for i in range(0, len(msg)):
147 s += chr(int(random()*26)+ord('A'))
156 return ' '*((LEN-len(str))/2)+str
159 def __init__(self, vendie):
160 # Each element of scrolling_message should be a 3-tuple of
161 # ('message', True/False if it is to be repeated, time to display)
162 self.scrolling_message = []
164 self.next_update = None
166 def set_message(self, string):
167 self.scrolling_message = [(string, False, None)]
168 self.update_display(True)
170 def set_messages(self, strings):
171 self.scrolling_message = strings
172 self.update_display(True)
174 def update_display(self, forced = False):
175 if not forced and self.next_update != None and time() < self.next_update:
177 if len(self.scrolling_message) > 0:
178 if len(self.scrolling_message[0][0]) > 10:
179 (m, r, t) = self.scrolling_message[0]
181 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
188 del self.scrolling_message[0]
189 self.scrolling_message = a + self.scrolling_message
190 newmsg = self.scrolling_message[0]
191 if newmsg[2] != None:
192 self.next_update = time() + newmsg[2]
194 self.next_update = None
195 self.v.display(self.scrolling_message[0][0])
196 if self.scrolling_message[0][1]:
197 self.scrolling_message.append(self.scrolling_message[0])
198 del self.scrolling_message[0]
201 return len(self.scrolling_message) == 0
203 def run_forever(rfh, wfh, options, cf):
204 v = VendingMachine(rfh, wfh)
205 print 'PING is', v.ping()
207 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
212 mk = MessageKeeper(v)
213 mk.set_message(GREETING)
214 time_to_autologout = None
215 #idler = TrainIdler(v)
216 #idler = GrayIdler(v)
217 idler = GrayIdler(v,one="*",zero="-")
219 last_timeout_refresh = None
222 if USE_DB: db.handle_events()
224 if time_to_autologout != None:
225 time_left = time_to_autologout - time()
226 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
227 mk.set_message('LOGOUT: '+str(int(time_left)))
228 last_timeout_refresh = int(time_left)
231 if time_to_autologout != None and time_to_autologout - time() <= 0:
232 time_to_autologout = None
236 mk.set_message(GREETING)
238 if time_to_autologout and not mk.done(): time_to_autologout = None
239 if cur_user == '' and time_to_autologout: time_to_autologout = None
240 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
242 time_to_autologout = time() + 15
244 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
245 if time_to_idle != None and cur_user != '': time_to_idle = None
246 if time_to_idle is not None and time() > time_to_idle: idler.next()
252 e = v.next_event(0.1)
263 mk.set_message(GREETING)
264 elif event == SWITCH:
265 # don't care right now.
269 # complicated key handling here:
270 if len(cur_user) < 5:
273 mk.set_message(GREETING)
275 cur_user += chr(key + ord('0'))
276 mk.set_message('UID: '+cur_user)
277 if len(cur_user) == 5:
279 if not has_good_pin(uid):
281 #[(center('INVALID'), False, 0.7),
282 #(center('PIN'), False, 0.7),
283 #(center('SETUP'), False, 1.0),
284 #(GREETING, False, None)])
286 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
287 (GREETING, False, None)])
292 mk.set_message('PIN: ')
294 elif len(cur_pin) < PIN_LENGTH:
298 mk.set_message(GREETING)
301 mk.set_message('PIN: ')
303 cur_pin += chr(key + ord('0'))
304 mk.set_message('PIN: '+'X'*len(cur_pin))
305 if len(cur_pin) == PIN_LENGTH:
306 username = verify_user_pin(int(cur_user), int(cur_pin))
310 scroll_options(username, mk, True)
315 [(center('BAD PIN'), False, 1.0),
316 (center('SORRY'), False, 0.5),
317 (GREETING, False, None)])
321 elif len(cur_selection) == 0:
327 [(center('BYE!'), False, 1.5),
328 (GREETING, False, None)])
330 cur_selection += chr(key + ord('0'))
331 mk.set_message('SELECT: '+cur_selection)
332 time_to_autologout = None
333 elif len(cur_selection) == 1:
336 time_to_autologout = None
337 scroll_options(username, mk)
340 cur_selection += chr(key + ord('0'))
341 #make_selection(cur_selection)
342 # XXX this should move somewhere else:
343 if cur_selection == '55':
344 mk.set_message('OPENSESAME')
346 ret = os.system('su - "%s" -c "dispense door"'%username)
348 ret = os.system('dispense door')
350 mk.set_message(center('DOOR OPEN'))
352 mk.set_message(center('BAD DOOR'))
354 elif cur_selection == '91':
356 elif cur_selection == '99':
357 scroll_options(username, mk)
360 elif cur_selection[1] == '8':
361 v.display('GOT COKE?')
362 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
364 v.display('HERES A '+cur_selection)
365 v.vend(cur_selection)
367 v.display('THANK YOU')
370 time_to_autologout = time() + 8
372 def connect_to_vend(options, cf):
373 # Open vending machine via LAT?
375 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
376 rfh, wfh = latclient.get_fh()
378 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
380 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
381 sock.connect((options.host, options.port))
382 rfh = sock.makefile('r')
383 wfh = sock.makefile('w')
388 from optparse import OptionParser
390 op = OptionParser(usage="%prog [OPTION]...")
391 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')
392 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
393 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
394 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
395 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
396 op.add_option('-s', '--syslog', dest='syslog', action='store_true', default=False, help='log output to syslog')
397 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
398 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
399 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
400 options, args = op.parse_args()
403 op.error('extra command line arguments: ' + ' '.join(args))
408 'DBServer': ('Database', 'Server'),
409 'DBName': ('Database', 'Name'),
410 'DBUser': ('VendingMachine', 'DBUser'),
411 'DBPassword': ('VendingMachine', 'DBPassword'),
413 'ServiceName': ('VendingMachine', 'ServiceName'),
414 'ServicePassword': ('VendingMachine', 'Password'),
416 'ServerName': ('DecServer', 'Name'),
417 'ConnectPassword': ('DecServer', 'ConnectPassword'),
418 'PrivPassword': ('DecServer', 'PrivPassword'),
421 class VendConfigFile:
422 def __init__(self, config_file, options):
424 cp = ConfigParser.ConfigParser()
427 for option in options:
428 section, name = options[option]
429 value = cp.get(section, name)
430 self.__dict__[option] = value
432 except ConfigParser.Error, e:
433 print "Error reading config file "+config_file+": " + str(e)
436 def create_pid_file(name):
438 pid_file = file(name, 'w')
439 pid_file.write('%d\n'%os.getpid())
442 logging.warning('unable to write to pid file '+name+': '+str(e))
445 def do_nothing(signum, stack): pass
446 def stop_server(signum, stack): raise KeyboardInterrupt
447 signal.signal(signal.SIGHUP, do_nothing)
448 signal.signal(signal.SIGTERM, stop_server)
449 signal.signal(signal.SIGINT, stop_server)
451 options = parse_args()
452 config_opts = VendConfigFile(options.config_file, config_options)
453 set_up_logging(options)
454 if options.daemon: become_daemon()
455 if options.pid_file != '': create_pid_file(options.pid_file)
457 return options, config_opts
459 def clean_up_nicely(options, config_opts):
460 if options.pid_file != '':
462 os.unlink(options.pid_file)
463 logging.debug('Removed pid file '+options.pid_file)
464 except OSError: pass # if we can't delete it, meh
466 def set_up_logging(options):
467 logger = logging.getLogger()
469 stderr_logger = logging.StreamHandler(sys.stderr)
470 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
471 logger.addHandler(stderr_logger)
473 if options.log_file != '':
475 file_logger = logging.FileHandler(options.log_file)
476 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
477 logger.addHandler(file_logger)
479 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
482 logger.setLevel(logging.DEBUG)
484 logger.setLevel(logging.INFO)
488 dev_null = file('/dev/null')
489 fd = dev_null.fileno()
496 def do_vend_server(options, config_opts):
499 rfh, wfh = connect_to_vend(options, config_opts)
500 except (LATClientException, socket.error), e:
501 (exc_type, exc_value, exc_traceback) = sys.exc_info()
504 print "Connection error: "+str(exc_type)+" "+str(e)
505 print "Trying again in 5 seconds."
509 run_forever(rfh, wfh, options, config_opts)
510 except VendingException:
512 print "Connection died, trying again..."
514 if __name__ == '__main__':
515 options, config_opts = set_stuff_up()
518 logging.info('Starting Vend Server')
519 do_vend_server(options, config_opts)
520 logging.warning('Vend Server finished unexpectedly, restarting')
521 except KeyboardInterrupt:
522 logging.info("Killed by signal, cleaning up")
523 clean_up_nicely(options, config_opts)
524 logging.info("Vend Server stopped")
527 (exc_type, exc_value, exc_traceback) = sys.exc_info()
528 tb = format_tb(exc_traceback, 20)
531 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
532 logging.critical("Message: " + str(exc_value))
533 logging.critical("Traceback:")
535 for line in event.split('\n'):
536 logging.critical(' '+line)
537 logging.critical("This message should be considered a bug in the Vend Server.")
538 logging.critical("Please report this to someone who can fix it.")
540 logging.warning("Trying again anyway (might not help, but hey...)")