7 import sys, os, string, re, pwd, signal, math
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 SerialClient import SerialClient, SerialClientException
15 from VendingMachine import VendingMachine, VendingException
16 from MessageKeeper import MessageKeeper
17 from HorizScroll import HorizScroll
18 from random import random, seed
19 from Idler import TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler
21 from posix import geteuid
24 This vending machine software brought to you by:
29 and a collective of hungry alpacas.
33 For a good time call +61 8 6488 3901
39 GREETING = 'UCC SNACKS'
46 class DispenseDatabaseException(Exception): pass
48 class DispenseDatabase:
49 def __init__(self, vending_machine, host, name, user, password):
50 self.vending_machine = vending_machine
51 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
52 self.db.query('LISTEN vend_requests')
54 def process_requests(self):
55 logging.debug('database processing')
56 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
58 outstanding = self.db.query(query).getresult()
59 except (pg.error,), db_err:
60 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
61 for (id, slot) in outstanding:
62 (worked, code, string) = self.vending_machine.vend(slot)
63 logging.debug (str((worked, code, string)))
65 query = 'SELECT vend_success(%s)'%id
66 self.db.query(query).getresult()
68 query = 'SELECT vend_failed(%s)'%id
69 self.db.query(query).getresult()
71 def handle_events(self):
72 notifier = self.db.getnotify()
73 while notifier is not None:
74 self.process_requests()
75 notify = self.db.getnotify()
77 def scroll_options(username, mk, welcome = False):
79 msg = [(center('WELCOME'), False, 0.8),
80 (center(username), False, 0.8)]
83 choices = ' '*10+'CHOICES: '
85 coke_machine = file('/home/other/coke/coke_contents')
86 cokes = coke_machine.readlines()
93 (slot_num, price, slot_name) = c.split(' ', 2)
94 if slot_name == 'dead': continue
95 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
97 choices += 'OR A SNACK. '
98 choices += '99 TO READ AGAIN. '
100 msg.append((choices, False, None))
105 info = pwd.getpwuid(uid)
107 logging.info('getting pin for uid %d: user not in password file'%uid)
109 if info.pw_dir == None: return False
110 pinfile = os.path.join(info.pw_dir, '.pin')
114 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
117 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
122 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
124 pinstr = f.readline()
126 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
127 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
131 def has_good_pin(uid):
132 return get_pin(uid) != None
134 def verify_user_pin(uid, pin):
135 if get_pin(uid) == pin:
136 info = pwd.getpwuid(uid)
137 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
140 logging.info('refused pin for uid %d'%(uid))
143 def door_open_mode(v):
144 logging.warning("Entering open door mode")
145 v.display("-FEED ME-")
151 if params == 1: # door closed
152 logging.warning('Leaving open door mode')
153 v.display("-YUM YUM!-")
159 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
160 choice = int(random()*len(messages))
161 msg = messages[choice]
162 left = range(len(msg))
163 for i in range(len(msg)):
164 if msg[i] == ' ': left.remove(i)
168 for i in range(0, len(msg)):
174 s += chr(int(random()*26)+ord('A'))
183 return ' '*((LEN-len(str))/2)+str
194 StringIdler(v, text="Kill 'em all", repeat=False),
195 GrayIdler(v,one="*",zero="-"),
196 StringIdler(v, text=CREDITS),
197 GrayIdler(v,one="/",zero="\\"),
198 FileIdler(v, '/etc/passwd'),
199 GrayIdler(v,one="X",zero="O"),
200 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
201 GrayIdler(v,one="*",zero="-",reorder=1),
202 StringIdler(v, text=str(math.pi) + " "),
203 GrayIdler(v,one="/",zero="\\",reorder=1),
204 StringIdler(v, text=str(math.e) + " "),
205 GrayIdler(v,one="X",zero="O",reorder=1),
213 idler = choose_idler()
220 iiindex = idlers.index(idler)
224 move = int(random()*len(idlers)) + 1
227 idler = idlers[( (iiindex + 1) % iilen)]
228 move = move - idler.affinity()
238 def run_forever(rfh, wfh, options, cf):
239 v = VendingMachine(rfh, wfh)
240 logging.debug('PING is ' + str(v.ping()))
242 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
247 mk = MessageKeeper(v)
248 mk.set_message(GREETING)
249 time_to_autologout = None
252 last_timeout_refresh = None
258 except DispenseDatabaseException, e:
259 logging.error('Database error: '+str(e))
261 if time_to_autologout != None:
262 time_left = time_to_autologout - time()
263 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
264 mk.set_message('LOGOUT: '+str(int(time_left)))
265 last_timeout_refresh = int(time_left)
268 if time_to_autologout != None and time_to_autologout - time() <= 0:
269 time_to_autologout = None
273 mk.set_message(GREETING)
275 if time_to_autologout and not mk.done(): time_to_autologout = None
276 if cur_user == '' and time_to_autologout: time_to_autologout = None
277 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
279 time_to_autologout = time() + 15
281 if time_to_idle == None and cur_user == '':
282 time_to_idle = time() + 5
284 if time_to_idle is not None and cur_user != '': time_to_idle = None
285 if time_to_idle is not None and time() > time_to_idle: idle_step()
286 if time_to_idle is not None and time() > time_to_idle + 300:
287 time_to_idle = time()
294 e = v.next_event(0.05)
299 logging.debug('Got event: ' + repr(e))
305 mk.set_message(GREETING)
306 elif event == SWITCH:
307 # don't care right now.
311 # complicated key handling here:
312 if len(cur_user) < 5:
315 mk.set_message(GREETING)
317 cur_user += chr(key + ord('0'))
318 mk.set_message('UID: '+cur_user)
319 if len(cur_user) == 5:
321 if not has_good_pin(uid):
322 logging.info('user '+cur_user+' has a bad PIN')
324 #[(center('INVALID'), False, 0.7),
325 #(center('PIN'), False, 0.7),
326 #(center('SETUP'), False, 1.0),
327 #(GREETING, False, None)])
329 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
330 (GREETING, False, None)])
335 mk.set_message('PIN: ')
336 logging.info('need pin for user %s'%cur_user)
338 elif len(cur_pin) < PIN_LENGTH:
342 mk.set_message(GREETING)
345 mk.set_message('PIN: ')
347 cur_pin += chr(key + ord('0'))
348 mk.set_message('PIN: '+'X'*len(cur_pin))
349 if len(cur_pin) == PIN_LENGTH:
350 username = verify_user_pin(int(cur_user), int(cur_pin))
354 scroll_options(username, mk, True)
359 [(center('BAD PIN'), False, 1.0),
360 (center('SORRY'), False, 0.5),
361 (GREETING, False, None)])
365 elif len(cur_selection) == 0:
371 [(center('BYE!'), False, 1.5),
372 (GREETING, False, None)])
374 cur_selection += chr(key + ord('0'))
375 mk.set_message('SELECT: '+cur_selection)
376 time_to_autologout = None
377 elif len(cur_selection) == 1:
380 time_to_autologout = None
381 scroll_options(username, mk)
384 cur_selection += chr(key + ord('0'))
385 #make_selection(cur_selection)
386 # XXX this should move somewhere else:
387 if cur_selection == '55':
388 mk.set_message('OPENSESAME')
389 logging.info('dispensing a door for %s'%username)
391 ret = os.system('su - "%s" -c "dispense door"'%username)
393 ret = os.system('dispense door')
395 logging.info('door opened')
396 mk.set_message(center('DOOR OPEN'))
398 logging.warning('user %s tried to dispense a bad door'%username)
399 mk.set_message(center('BAD DOOR'))
401 elif cur_selection == '91':
403 elif cur_selection == '99':
404 scroll_options(username, mk)
407 elif cur_selection[1] == '8':
408 v.display('GOT COKE?')
409 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
411 v.display('HERES A '+cur_selection)
412 v.vend(cur_selection)
414 v.display('THANK YOU')
417 time_to_autologout = time() + 8
419 def connect_to_vend(options, cf):
420 # Open vending machine via serial.
421 logging.info('Connecting to vending machine using serial')
422 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
423 return serialclient.get_fh()
426 logging.info('Connecting to vending machine using LAT')
427 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
428 rfh, wfh = latclient.get_fh()
430 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
431 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
433 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
434 sock.connect((options.host, options.port))
435 rfh = sock.makefile('r')
436 wfh = sock.makefile('w')
441 from optparse import OptionParser
443 op = OptionParser(usage="%prog [OPTION]...")
444 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')
445 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
446 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
447 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
448 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
449 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
450 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
451 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
452 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
453 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
454 options, args = op.parse_args()
457 op.error('extra command line arguments: ' + ' '.join(args))
462 'DBServer': ('Database', 'Server'),
463 'DBName': ('Database', 'Name'),
464 'DBUser': ('VendingMachine', 'DBUser'),
465 'DBPassword': ('VendingMachine', 'DBPassword'),
467 'ServiceName': ('VendingMachine', 'ServiceName'),
468 'ServicePassword': ('VendingMachine', 'Password'),
470 'ServerName': ('DecServer', 'Name'),
471 'ConnectPassword': ('DecServer', 'ConnectPassword'),
472 'PrivPassword': ('DecServer', 'PrivPassword'),
475 class VendConfigFile:
476 def __init__(self, config_file, options):
478 cp = ConfigParser.ConfigParser()
481 for option in options:
482 section, name = options[option]
483 value = cp.get(section, name)
484 self.__dict__[option] = value
486 except ConfigParser.Error, e:
487 raise SystemExit("Error reading config file "+config_file+": " + str(e))
489 def create_pid_file(name):
491 pid_file = file(name, 'w')
492 pid_file.write('%d\n'%os.getpid())
495 logging.warning('unable to write to pid file '+name+': '+str(e))
498 def do_nothing(signum, stack):
499 signal.signal(signum, do_nothing)
500 def stop_server(signum, stack): raise KeyboardInterrupt
501 signal.signal(signal.SIGHUP, do_nothing)
502 signal.signal(signal.SIGTERM, stop_server)
503 signal.signal(signal.SIGINT, stop_server)
505 options = parse_args()
506 config_opts = VendConfigFile(options.config_file, config_options)
507 if options.daemon: become_daemon()
508 set_up_logging(options)
509 if options.pid_file != '': create_pid_file(options.pid_file)
511 return options, config_opts
513 def clean_up_nicely(options, config_opts):
514 if options.pid_file != '':
516 os.unlink(options.pid_file)
517 logging.debug('Removed pid file '+options.pid_file)
518 except OSError: pass # if we can't delete it, meh
520 def set_up_logging(options):
521 logger = logging.getLogger()
523 if not options.daemon:
524 stderr_logger = logging.StreamHandler(sys.stderr)
525 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
526 logger.addHandler(stderr_logger)
528 if options.log_file != '':
530 file_logger = logging.FileHandler(options.log_file)
531 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
532 logger.addHandler(file_logger)
534 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
536 if options.syslog != None:
537 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
538 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
539 logger.addHandler(sys_logger)
542 logger.setLevel(logging.WARNING)
543 elif options.verbose:
544 logger.setLevel(logging.DEBUG)
546 logger.setLevel(logging.INFO)
549 dev_null = file('/dev/null')
550 fd = dev_null.fileno()
559 raise SystemExit('failed to fork: '+str(e))
561 def do_vend_server(options, config_opts):
564 rfh, wfh = connect_to_vend(options, config_opts)
565 except (SerialClientException, socket.error), e:
566 (exc_type, exc_value, exc_traceback) = sys.exc_info()
568 logging.error("Connection error: "+str(exc_type)+" "+str(e))
569 logging.info("Trying again in 5 seconds.")
574 run_forever(rfh, wfh, options, config_opts)
575 except VendingException:
576 logging.error("Connection died, trying again...")
577 logging.info("Trying again in 5 seconds.")
580 if __name__ == '__main__':
581 options, config_opts = set_stuff_up()
584 logging.warning('Starting Vend Server')
585 do_vend_server(options, config_opts)
586 logging.error('Vend Server finished unexpectedly, restarting')
587 except KeyboardInterrupt:
588 logging.info("Killed by signal, cleaning up")
589 clean_up_nicely(options, config_opts)
590 logging.warning("Vend Server stopped")
595 (exc_type, exc_value, exc_traceback) = sys.exc_info()
596 tb = format_tb(exc_traceback, 20)
599 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
600 logging.critical("Message: " + str(exc_value))
601 logging.critical("Traceback:")
603 for line in event.split('\n'):
604 logging.critical(' '+line)
605 logging.critical("This message should be considered a bug in the Vend Server.")
606 logging.critical("Please report this to someone who can fix it.")
608 logging.warning("Trying again anyway (might not help, but hey...)")