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="\\"),
199 FileIdler(v, '/etc/passwd'),
200 GrayIdler(v,one="X",zero="O"),
201 FileIdler(v, '/usr/share/common-licenses/GPL-2'),
202 GrayIdler(v,one="*",zero="-",reorder=1),
203 StringIdler(v, text=str(math.pi) + " "),
205 GrayIdler(v,one="/",zero="\\",reorder=1),
206 StringIdler(v, text=str(math.e) + " "),
207 GrayIdler(v,one="X",zero="O",reorder=1),
208 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 - and order as Quinn - I am getting really hungry", repeat=False),
209 PipeIdler(v, "/usr/bin/ypcat", "passwd"),
217 idler = choose_idler()
224 iiindex = idlers.index(idler)
228 move = int(random()*len(idlers)) + 1
231 idler = idlers[( (iiindex + 1) % iilen)]
232 move = move - idler.affinity()
242 def run_forever(rfh, wfh, options, cf):
243 v = VendingMachine(rfh, wfh)
244 logging.debug('PING is ' + str(v.ping()))
246 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
251 mk = MessageKeeper(v)
252 mk.set_message(GREETING)
253 time_to_autologout = None
256 last_timeout_refresh = None
262 except DispenseDatabaseException, e:
263 logging.error('Database error: '+str(e))
265 if time_to_autologout != None:
266 time_left = time_to_autologout - time()
267 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
268 mk.set_message('LOGOUT: '+str(int(time_left)))
269 last_timeout_refresh = int(time_left)
272 if time_to_autologout != None and time_to_autologout - time() <= 0:
273 time_to_autologout = None
277 mk.set_message(GREETING)
279 if time_to_autologout and not mk.done(): time_to_autologout = None
280 if cur_user == '' and time_to_autologout: time_to_autologout = None
281 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
283 time_to_autologout = time() + 15
284 last_timeout_refresh = None
286 if time_to_idle == None and cur_user == '':
287 time_to_idle = time() + 5
289 if time_to_idle is not None and cur_user != '': time_to_idle = None
290 if time_to_idle is not None and time() > time_to_idle: idle_step()
291 if time_to_idle is not None and time() > time_to_idle + 300:
292 time_to_idle = time()
299 e = v.next_event(0.05)
304 logging.debug('Got event: ' + repr(e))
310 mk.set_message(GREETING)
311 elif event == SWITCH:
312 # don't care right now.
316 # complicated key handling here:
317 if len(cur_user) < 5:
320 mk.set_message(GREETING)
322 cur_user += chr(key + ord('0'))
323 mk.set_message('UID: '+cur_user)
324 if len(cur_user) == 5:
326 if not has_good_pin(uid):
327 logging.info('user '+cur_user+' has a bad PIN')
329 #[(center('INVALID'), False, 0.7),
330 #(center('PIN'), False, 0.7),
331 #(center('SETUP'), False, 1.0),
332 #(GREETING, False, None)])
334 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
335 (GREETING, False, None)])
340 mk.set_message('PIN: ')
341 logging.info('need pin for user %s'%cur_user)
343 elif len(cur_pin) < PIN_LENGTH:
347 mk.set_message(GREETING)
350 mk.set_message('PIN: ')
352 cur_pin += chr(key + ord('0'))
353 mk.set_message('PIN: '+'X'*len(cur_pin))
354 if len(cur_pin) == PIN_LENGTH:
355 username = verify_user_pin(int(cur_user), int(cur_pin))
359 scroll_options(username, mk, True)
364 [(center('BAD PIN'), False, 1.0),
365 (center('SORRY'), False, 0.5),
366 (GREETING, False, None)])
370 elif len(cur_selection) == 0:
376 [(center('BYE!'), False, 1.5),
377 (GREETING, False, None)])
379 cur_selection += chr(key + ord('0'))
380 mk.set_message('SELECT: '+cur_selection)
381 time_to_autologout = None
382 elif len(cur_selection) == 1:
385 time_to_autologout = None
386 scroll_options(username, mk)
389 cur_selection += chr(key + ord('0'))
390 #make_selection(cur_selection)
391 # XXX this should move somewhere else:
392 if cur_selection == '55':
393 mk.set_message('OPENSESAME')
394 logging.info('dispensing a door for %s'%username)
396 ret = os.system('su - "%s" -c "dispense door"'%username)
398 ret = os.system('dispense door')
400 logging.info('door opened')
401 mk.set_message(center('DOOR OPEN'))
403 logging.warning('user %s tried to dispense a bad door'%username)
404 mk.set_message(center('BAD DOOR'))
406 elif cur_selection == '91':
408 elif cur_selection == '99':
409 scroll_options(username, mk)
412 elif cur_selection[1] == '8':
413 v.display('GOT COKE?')
414 if ((os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0])) >> 8) != 0):
415 v.display('SEEMS NOT')
417 v.display('GOT COKE!')
419 v.display(cur_selection+' - $1.00')
420 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
421 v.vend(cur_selection)
422 v.display('THANK YOU')
424 v.display('NO MONEY?')
427 time_to_autologout = time() + 8
428 last_timeout_refresh = None
430 def connect_to_vend(options, cf):
433 logging.info('Connecting to vending machine using LAT')
434 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
435 rfh, wfh = latclient.get_fh()
436 elif options.use_serial:
437 # Open vending machine via serial.
438 logging.info('Connecting to vending machine using serial')
439 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
440 rfh,wfh = serialclient.get_fh()
442 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
443 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
445 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
446 sock.connect((options.host, options.port))
447 rfh = sock.makefile('r')
448 wfh = sock.makefile('w')
453 from optparse import OptionParser
455 op = OptionParser(usage="%prog [OPTION]...")
456 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')
457 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
458 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
459 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
460 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
461 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
462 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
463 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
464 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
465 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
466 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
467 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
468 options, args = op.parse_args()
471 op.error('extra command line arguments: ' + ' '.join(args))
476 'DBServer': ('Database', 'Server'),
477 'DBName': ('Database', 'Name'),
478 'DBUser': ('VendingMachine', 'DBUser'),
479 'DBPassword': ('VendingMachine', 'DBPassword'),
481 'ServiceName': ('VendingMachine', 'ServiceName'),
482 'ServicePassword': ('VendingMachine', 'Password'),
484 'ServerName': ('DecServer', 'Name'),
485 'ConnectPassword': ('DecServer', 'ConnectPassword'),
486 'PrivPassword': ('DecServer', 'PrivPassword'),
489 class VendConfigFile:
490 def __init__(self, config_file, options):
492 cp = ConfigParser.ConfigParser()
495 for option in options:
496 section, name = options[option]
497 value = cp.get(section, name)
498 self.__dict__[option] = value
500 except ConfigParser.Error, e:
501 raise SystemExit("Error reading config file "+config_file+": " + str(e))
503 def create_pid_file(name):
505 pid_file = file(name, 'w')
506 pid_file.write('%d\n'%os.getpid())
509 logging.warning('unable to write to pid file '+name+': '+str(e))
512 def do_nothing(signum, stack):
513 signal.signal(signum, do_nothing)
514 def stop_server(signum, stack): raise KeyboardInterrupt
515 signal.signal(signal.SIGHUP, do_nothing)
516 signal.signal(signal.SIGTERM, stop_server)
517 signal.signal(signal.SIGINT, stop_server)
519 options = parse_args()
520 config_opts = VendConfigFile(options.config_file, config_options)
521 if options.daemon: become_daemon()
522 set_up_logging(options)
523 if options.pid_file != '': create_pid_file(options.pid_file)
525 return options, config_opts
527 def clean_up_nicely(options, config_opts):
528 if options.pid_file != '':
530 os.unlink(options.pid_file)
531 logging.debug('Removed pid file '+options.pid_file)
532 except OSError: pass # if we can't delete it, meh
534 def set_up_logging(options):
535 logger = logging.getLogger()
537 if not options.daemon:
538 stderr_logger = logging.StreamHandler(sys.stderr)
539 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
540 logger.addHandler(stderr_logger)
542 if options.log_file != '':
544 file_logger = logging.FileHandler(options.log_file)
545 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
546 logger.addHandler(file_logger)
548 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
550 if options.syslog != None:
551 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
552 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
553 logger.addHandler(sys_logger)
556 logger.setLevel(logging.WARNING)
557 elif options.verbose:
558 logger.setLevel(logging.DEBUG)
560 logger.setLevel(logging.INFO)
563 dev_null = file('/dev/null')
564 fd = dev_null.fileno()
573 raise SystemExit('failed to fork: '+str(e))
575 def do_vend_server(options, config_opts):
578 rfh, wfh = connect_to_vend(options, config_opts)
579 except (SerialClientException, socket.error), e:
580 (exc_type, exc_value, exc_traceback) = sys.exc_info()
582 logging.error("Connection error: "+str(exc_type)+" "+str(e))
583 logging.info("Trying again in 5 seconds.")
588 run_forever(rfh, wfh, options, config_opts)
589 except VendingException:
590 logging.error("Connection died, trying again...")
591 logging.info("Trying again in 5 seconds.")
594 if __name__ == '__main__':
595 options, config_opts = set_stuff_up()
598 logging.warning('Starting Vend Server')
599 do_vend_server(options, config_opts)
600 logging.error('Vend Server finished unexpectedly, restarting')
601 except KeyboardInterrupt:
602 logging.info("Killed by signal, cleaning up")
603 clean_up_nicely(options, config_opts)
604 logging.warning("Vend Server stopped")
609 (exc_type, exc_value, exc_traceback) = sys.exc_info()
610 tb = format_tb(exc_traceback, 20)
613 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
614 logging.critical("Message: " + str(exc_value))
615 logging.critical("Traceback:")
617 for line in event.split('\n'):
618 logging.critical(' '+line)
619 logging.critical("This message should be considered a bug in the Vend Server.")
620 logging.critical("Please report this to someone who can fix it.")
622 logging.warning("Trying again anyway (might not help, but hey...)")