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
280 last_timeout_refresh = None
282 if time_to_idle == None and cur_user == '':
283 time_to_idle = time() + 5
285 if time_to_idle is not None and cur_user != '': time_to_idle = None
286 if time_to_idle is not None and time() > time_to_idle: idle_step()
287 if time_to_idle is not None and time() > time_to_idle + 300:
288 time_to_idle = time()
295 e = v.next_event(0.05)
300 logging.debug('Got event: ' + repr(e))
306 mk.set_message(GREETING)
307 elif event == SWITCH:
308 # don't care right now.
312 # complicated key handling here:
313 if len(cur_user) < 5:
316 mk.set_message(GREETING)
318 cur_user += chr(key + ord('0'))
319 mk.set_message('UID: '+cur_user)
320 if len(cur_user) == 5:
322 if not has_good_pin(uid):
323 logging.info('user '+cur_user+' has a bad PIN')
325 #[(center('INVALID'), False, 0.7),
326 #(center('PIN'), False, 0.7),
327 #(center('SETUP'), False, 1.0),
328 #(GREETING, False, None)])
330 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
331 (GREETING, False, None)])
336 mk.set_message('PIN: ')
337 logging.info('need pin for user %s'%cur_user)
339 elif len(cur_pin) < PIN_LENGTH:
343 mk.set_message(GREETING)
346 mk.set_message('PIN: ')
348 cur_pin += chr(key + ord('0'))
349 mk.set_message('PIN: '+'X'*len(cur_pin))
350 if len(cur_pin) == PIN_LENGTH:
351 username = verify_user_pin(int(cur_user), int(cur_pin))
355 scroll_options(username, mk, True)
360 [(center('BAD PIN'), False, 1.0),
361 (center('SORRY'), False, 0.5),
362 (GREETING, False, None)])
366 elif len(cur_selection) == 0:
372 [(center('BYE!'), False, 1.5),
373 (GREETING, False, None)])
375 cur_selection += chr(key + ord('0'))
376 mk.set_message('SELECT: '+cur_selection)
377 time_to_autologout = None
378 elif len(cur_selection) == 1:
381 time_to_autologout = None
382 scroll_options(username, mk)
385 cur_selection += chr(key + ord('0'))
386 #make_selection(cur_selection)
387 # XXX this should move somewhere else:
388 if cur_selection == '55':
389 mk.set_message('OPENSESAME')
390 logging.info('dispensing a door for %s'%username)
392 ret = os.system('su - "%s" -c "dispense door"'%username)
394 ret = os.system('dispense door')
396 logging.info('door opened')
397 mk.set_message(center('DOOR OPEN'))
399 logging.warning('user %s tried to dispense a bad door'%username)
400 mk.set_message(center('BAD DOOR'))
402 elif cur_selection == '91':
404 elif cur_selection == '99':
405 scroll_options(username, mk)
408 elif cur_selection[1] == '8':
409 v.display('GOT COKE?')
410 if ((os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0])) >> 8) != 0):
411 v.display('SEEMS NOT')
413 v.display('GOT COKE!')
415 v.display(cur_selection+' - $1.00')
416 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
417 v.vend(cur_selection)
418 v.display('THANK YOU')
420 v.display('NO MONEY?')
423 time_to_autologout = time() + 8
424 last_timeout_refresh = None
426 def connect_to_vend(options, cf):
429 logging.info('Connecting to vending machine using LAT')
430 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
431 rfh, wfh = latclient.get_fh()
432 elif options.use_serial:
433 # Open vending machine via serial.
434 logging.info('Connecting to vending machine using serial')
435 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
436 rfh,wfh = serialclient.get_fh()
438 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
439 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
441 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
442 sock.connect((options.host, options.port))
443 rfh = sock.makefile('r')
444 wfh = sock.makefile('w')
449 from optparse import OptionParser
451 op = OptionParser(usage="%prog [OPTION]...")
452 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')
453 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
454 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
455 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
456 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
457 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
458 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
459 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
460 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
461 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
462 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
463 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
464 options, args = op.parse_args()
467 op.error('extra command line arguments: ' + ' '.join(args))
472 'DBServer': ('Database', 'Server'),
473 'DBName': ('Database', 'Name'),
474 'DBUser': ('VendingMachine', 'DBUser'),
475 'DBPassword': ('VendingMachine', 'DBPassword'),
477 'ServiceName': ('VendingMachine', 'ServiceName'),
478 'ServicePassword': ('VendingMachine', 'Password'),
480 'ServerName': ('DecServer', 'Name'),
481 'ConnectPassword': ('DecServer', 'ConnectPassword'),
482 'PrivPassword': ('DecServer', 'PrivPassword'),
485 class VendConfigFile:
486 def __init__(self, config_file, options):
488 cp = ConfigParser.ConfigParser()
491 for option in options:
492 section, name = options[option]
493 value = cp.get(section, name)
494 self.__dict__[option] = value
496 except ConfigParser.Error, e:
497 raise SystemExit("Error reading config file "+config_file+": " + str(e))
499 def create_pid_file(name):
501 pid_file = file(name, 'w')
502 pid_file.write('%d\n'%os.getpid())
505 logging.warning('unable to write to pid file '+name+': '+str(e))
508 def do_nothing(signum, stack):
509 signal.signal(signum, do_nothing)
510 def stop_server(signum, stack): raise KeyboardInterrupt
511 signal.signal(signal.SIGHUP, do_nothing)
512 signal.signal(signal.SIGTERM, stop_server)
513 signal.signal(signal.SIGINT, stop_server)
515 options = parse_args()
516 config_opts = VendConfigFile(options.config_file, config_options)
517 if options.daemon: become_daemon()
518 set_up_logging(options)
519 if options.pid_file != '': create_pid_file(options.pid_file)
521 return options, config_opts
523 def clean_up_nicely(options, config_opts):
524 if options.pid_file != '':
526 os.unlink(options.pid_file)
527 logging.debug('Removed pid file '+options.pid_file)
528 except OSError: pass # if we can't delete it, meh
530 def set_up_logging(options):
531 logger = logging.getLogger()
533 if not options.daemon:
534 stderr_logger = logging.StreamHandler(sys.stderr)
535 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
536 logger.addHandler(stderr_logger)
538 if options.log_file != '':
540 file_logger = logging.FileHandler(options.log_file)
541 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
542 logger.addHandler(file_logger)
544 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
546 if options.syslog != None:
547 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
548 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
549 logger.addHandler(sys_logger)
552 logger.setLevel(logging.WARNING)
553 elif options.verbose:
554 logger.setLevel(logging.DEBUG)
556 logger.setLevel(logging.INFO)
559 dev_null = file('/dev/null')
560 fd = dev_null.fileno()
569 raise SystemExit('failed to fork: '+str(e))
571 def do_vend_server(options, config_opts):
574 rfh, wfh = connect_to_vend(options, config_opts)
575 except (SerialClientException, socket.error), e:
576 (exc_type, exc_value, exc_traceback) = sys.exc_info()
578 logging.error("Connection error: "+str(exc_type)+" "+str(e))
579 logging.info("Trying again in 5 seconds.")
584 run_forever(rfh, wfh, options, config_opts)
585 except VendingException:
586 logging.error("Connection died, trying again...")
587 logging.info("Trying again in 5 seconds.")
590 if __name__ == '__main__':
591 options, config_opts = set_stuff_up()
594 logging.warning('Starting Vend Server')
595 do_vend_server(options, config_opts)
596 logging.error('Vend Server finished unexpectedly, restarting')
597 except KeyboardInterrupt:
598 logging.info("Killed by signal, cleaning up")
599 clean_up_nicely(options, config_opts)
600 logging.warning("Vend Server stopped")
605 (exc_type, exc_value, exc_traceback) = sys.exc_info()
606 tb = format_tb(exc_traceback, 20)
609 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
610 logging.critical("Message: " + str(exc_value))
611 logging.critical("Traceback:")
613 for line in event.split('\n'):
614 logging.critical(' '+line)
615 logging.critical("This message should be considered a bug in the Vend Server.")
616 logging.critical("Please report this to someone who can fix it.")
618 logging.warning("Trying again anyway (might not help, but hey...)")