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),
216 idler = choose_idler()
223 iiindex = idlers.index(idler)
227 move = int(random()*len(idlers)) + 1
230 idler = idlers[( (iiindex + 1) % iilen)]
231 move = move - idler.affinity()
241 def run_forever(rfh, wfh, options, cf):
242 v = VendingMachine(rfh, wfh)
243 logging.debug('PING is ' + str(v.ping()))
245 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
250 mk = MessageKeeper(v)
251 mk.set_message(GREETING)
252 time_to_autologout = None
255 last_timeout_refresh = None
261 except DispenseDatabaseException, e:
262 logging.error('Database error: '+str(e))
264 if time_to_autologout != None:
265 time_left = time_to_autologout - time()
266 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
267 mk.set_message('LOGOUT: '+str(int(time_left)))
268 last_timeout_refresh = int(time_left)
271 if time_to_autologout != None and time_to_autologout - time() <= 0:
272 time_to_autologout = None
276 mk.set_message(GREETING)
278 if time_to_autologout and not mk.done(): time_to_autologout = None
279 if cur_user == '' and time_to_autologout: time_to_autologout = None
280 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
282 time_to_autologout = time() + 15
283 last_timeout_refresh = None
285 if time_to_idle == None and cur_user == '':
286 time_to_idle = time() + 5
288 if time_to_idle is not None and cur_user != '': time_to_idle = None
289 if time_to_idle is not None and time() > time_to_idle: idle_step()
290 if time_to_idle is not None and time() > time_to_idle + 300:
291 time_to_idle = time()
298 e = v.next_event(0.05)
303 logging.debug('Got event: ' + repr(e))
309 mk.set_message(GREETING)
310 elif event == SWITCH:
311 # don't care right now.
315 # complicated key handling here:
316 if len(cur_user) < 5:
319 mk.set_message(GREETING)
321 cur_user += chr(key + ord('0'))
322 mk.set_message('UID: '+cur_user)
323 if len(cur_user) == 5:
325 if not has_good_pin(uid):
326 logging.info('user '+cur_user+' has a bad PIN')
328 #[(center('INVALID'), False, 0.7),
329 #(center('PIN'), False, 0.7),
330 #(center('SETUP'), False, 1.0),
331 #(GREETING, False, None)])
333 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
334 (GREETING, False, None)])
339 mk.set_message('PIN: ')
340 logging.info('need pin for user %s'%cur_user)
342 elif len(cur_pin) < PIN_LENGTH:
346 mk.set_message(GREETING)
349 mk.set_message('PIN: ')
351 cur_pin += chr(key + ord('0'))
352 mk.set_message('PIN: '+'X'*len(cur_pin))
353 if len(cur_pin) == PIN_LENGTH:
354 username = verify_user_pin(int(cur_user), int(cur_pin))
358 scroll_options(username, mk, True)
363 [(center('BAD PIN'), False, 1.0),
364 (center('SORRY'), False, 0.5),
365 (GREETING, False, None)])
369 elif len(cur_selection) == 0:
375 [(center('BYE!'), False, 1.5),
376 (GREETING, False, None)])
378 cur_selection += chr(key + ord('0'))
379 mk.set_message('SELECT: '+cur_selection)
380 time_to_autologout = None
381 elif len(cur_selection) == 1:
384 time_to_autologout = None
385 scroll_options(username, mk)
388 cur_selection += chr(key + ord('0'))
389 #make_selection(cur_selection)
390 # XXX this should move somewhere else:
391 if cur_selection == '55':
392 mk.set_message('OPENSESAME')
393 logging.info('dispensing a door for %s'%username)
395 ret = os.system('su - "%s" -c "dispense door"'%username)
397 ret = os.system('dispense door')
399 logging.info('door opened')
400 mk.set_message(center('DOOR OPEN'))
402 logging.warning('user %s tried to dispense a bad door'%username)
403 mk.set_message(center('BAD DOOR'))
405 elif cur_selection == '91':
407 elif cur_selection == '99':
408 scroll_options(username, mk)
411 elif cur_selection[1] == '8':
412 v.display('GOT COKE?')
413 if ((os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0])) >> 8) != 0):
414 v.display('SEEMS NOT')
416 v.display('GOT COKE!')
418 v.display(cur_selection+' - $1.00')
419 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
420 v.vend(cur_selection)
421 v.display('THANK YOU')
423 v.display('NO MONEY?')
426 time_to_autologout = time() + 8
427 last_timeout_refresh = None
429 def connect_to_vend(options, cf):
432 logging.info('Connecting to vending machine using LAT')
433 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
434 rfh, wfh = latclient.get_fh()
435 elif options.use_serial:
436 # Open vending machine via serial.
437 logging.info('Connecting to vending machine using serial')
438 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
439 rfh,wfh = serialclient.get_fh()
441 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
442 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
444 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
445 sock.connect((options.host, options.port))
446 rfh = sock.makefile('r')
447 wfh = sock.makefile('w')
452 from optparse import OptionParser
454 op = OptionParser(usage="%prog [OPTION]...")
455 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')
456 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
457 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
458 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
459 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
460 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
461 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
462 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
463 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
464 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
465 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
466 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
467 options, args = op.parse_args()
470 op.error('extra command line arguments: ' + ' '.join(args))
475 'DBServer': ('Database', 'Server'),
476 'DBName': ('Database', 'Name'),
477 'DBUser': ('VendingMachine', 'DBUser'),
478 'DBPassword': ('VendingMachine', 'DBPassword'),
480 'ServiceName': ('VendingMachine', 'ServiceName'),
481 'ServicePassword': ('VendingMachine', 'Password'),
483 'ServerName': ('DecServer', 'Name'),
484 'ConnectPassword': ('DecServer', 'ConnectPassword'),
485 'PrivPassword': ('DecServer', 'PrivPassword'),
488 class VendConfigFile:
489 def __init__(self, config_file, options):
491 cp = ConfigParser.ConfigParser()
494 for option in options:
495 section, name = options[option]
496 value = cp.get(section, name)
497 self.__dict__[option] = value
499 except ConfigParser.Error, e:
500 raise SystemExit("Error reading config file "+config_file+": " + str(e))
502 def create_pid_file(name):
504 pid_file = file(name, 'w')
505 pid_file.write('%d\n'%os.getpid())
508 logging.warning('unable to write to pid file '+name+': '+str(e))
511 def do_nothing(signum, stack):
512 signal.signal(signum, do_nothing)
513 def stop_server(signum, stack): raise KeyboardInterrupt
514 signal.signal(signal.SIGHUP, do_nothing)
515 signal.signal(signal.SIGTERM, stop_server)
516 signal.signal(signal.SIGINT, stop_server)
518 options = parse_args()
519 config_opts = VendConfigFile(options.config_file, config_options)
520 if options.daemon: become_daemon()
521 set_up_logging(options)
522 if options.pid_file != '': create_pid_file(options.pid_file)
524 return options, config_opts
526 def clean_up_nicely(options, config_opts):
527 if options.pid_file != '':
529 os.unlink(options.pid_file)
530 logging.debug('Removed pid file '+options.pid_file)
531 except OSError: pass # if we can't delete it, meh
533 def set_up_logging(options):
534 logger = logging.getLogger()
536 if not options.daemon:
537 stderr_logger = logging.StreamHandler(sys.stderr)
538 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
539 logger.addHandler(stderr_logger)
541 if options.log_file != '':
543 file_logger = logging.FileHandler(options.log_file)
544 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
545 logger.addHandler(file_logger)
547 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
549 if options.syslog != None:
550 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
551 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
552 logger.addHandler(sys_logger)
555 logger.setLevel(logging.WARNING)
556 elif options.verbose:
557 logger.setLevel(logging.DEBUG)
559 logger.setLevel(logging.INFO)
562 dev_null = file('/dev/null')
563 fd = dev_null.fileno()
572 raise SystemExit('failed to fork: '+str(e))
574 def do_vend_server(options, config_opts):
577 rfh, wfh = connect_to_vend(options, config_opts)
578 except (SerialClientException, socket.error), e:
579 (exc_type, exc_value, exc_traceback) = sys.exc_info()
581 logging.error("Connection error: "+str(exc_type)+" "+str(e))
582 logging.info("Trying again in 5 seconds.")
587 run_forever(rfh, wfh, options, config_opts)
588 except VendingException:
589 logging.error("Connection died, trying again...")
590 logging.info("Trying again in 5 seconds.")
593 if __name__ == '__main__':
594 options, config_opts = set_stuff_up()
597 logging.warning('Starting Vend Server')
598 do_vend_server(options, config_opts)
599 logging.error('Vend Server finished unexpectedly, restarting')
600 except KeyboardInterrupt:
601 logging.info("Killed by signal, cleaning up")
602 clean_up_nicely(options, config_opts)
603 logging.warning("Vend Server stopped")
608 (exc_type, exc_value, exc_traceback) = sys.exc_info()
609 tb = format_tb(exc_traceback, 20)
612 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
613 logging.critical("Message: " + str(exc_value))
614 logging.critical("Traceback:")
616 for line in event.split('\n'):
617 logging.critical(' '+line)
618 logging.critical("This message should be considered a bug in the Vend Server.")
619 logging.critical("Please report this to someone who can fix it.")
621 logging.warning("Trying again anyway (might not help, but hey...)")