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,PipeIdler
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 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) + " "),
204 GrayIdler(v,one="/",zero="\\",reorder=1),
205 StringIdler(v, text=str(math.e) + " "),
206 GrayIdler(v,one="X",zero="O",reorder=1),
207 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),
208 PipeIdler(v, "/usr/bin/ypcat", "passwd"),
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()
242 def __init__(self,v):
243 self.mk = MessageKeeper(v)
246 self.cur_selection = ''
247 self.time_to_autologout = None
248 self.time_to_idle = None
249 self.last_timeout_refresh = None
252 def handle_door_event(event, params, v, vstatus):
255 vstatus.cur_user = ''
257 vstatus.mk.set_message(GREETING)
259 def handle_switch_event(event, params, v, vstatus):
260 # don't care right now.
263 def handle_key_event(event, params, v, vstatus):
265 # complicated key handling here:
266 if len(vstatus.cur_user) < 5:
268 vstatus.cur_user = ''
269 vstatus.mk.set_message(GREETING)
271 vstatus.cur_user += chr(key + ord('0'))
272 vstatus.mk.set_message('UID: '+vstatus.cur_user)
273 if len(vstatus.cur_user) == 5:
274 uid = int(vstatus.cur_user)
275 if not has_good_pin(uid):
276 logging.info('user '+vstatus.cur_user+' has a bad PIN')
278 #[(center('INVALID'), False, 0.7),
279 #(center('PIN'), False, 0.7),
280 #(center('SETUP'), False, 1.0),
281 #(GREETING, False, None)])
282 vstatus.mk.set_messages(
283 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
284 (GREETING, False, None)])
285 vstatus.cur_user = ''
289 vstatus.mk.set_message('PIN: ')
290 logging.info('need pin for user %s'%vstatus.cur_user)
292 elif len(vstatus.cur_pin) < PIN_LENGTH:
294 if vstatus.cur_pin == '':
295 vstatus.cur_user = ''
296 vstatus.mk.set_message(GREETING)
299 vstatus.mk.set_message('PIN: ')
301 vstatus.cur_pin += chr(key + ord('0'))
302 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
303 if len(vstatus.cur_pin) == PIN_LENGTH:
304 username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
307 vstatus.cur_selection = ''
308 scroll_options(username, vstatus.mk, True)
312 vstatus.mk.set_messages(
313 [(center('BAD PIN'), False, 1.0),
314 (center('SORRY'), False, 0.5),
315 (GREETING, False, None)])
316 vstatus.cur_user = ''
319 elif len(vstatus.cur_selection) == 0:
322 vstatus.cur_user = ''
323 vstatus.cur_selection = ''
324 vstatus.mk.set_messages(
325 [(center('BYE!'), False, 1.5),
326 (GREETING, False, None)])
328 vstatus.cur_selection += chr(key + ord('0'))
329 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
330 vstatus.time_to_autologout = None
331 elif len(vstatus.cur_selection) == 1:
333 vstatus.cur_selection = ''
334 vstatus.time_to_autologout = None
335 scroll_options(username, vstatus.mk)
338 vstatus.cur_selection += chr(key + ord('0'))
339 #make_selection(cur_selection)
340 # XXX this should move somewhere else:
341 if vstatus.cur_selection == '55':
342 vstatus.mk.set_message('OPENSESAME')
343 logging.info('dispensing a door for %s'%username)
345 ret = os.system('su - "%s" -c "dispense door"'%username)
347 ret = os.system('dispense door')
349 logging.info('door opened')
350 vstatus.mk.set_message(center('DOOR OPEN'))
352 logging.warning('user %s tried to dispense a bad door'%username)
353 vstatus.mk.set_message(center('BAD DOOR'))
355 elif vstatus.cur_selection == '91':
357 elif vstatus.cur_selection == '99':
358 scroll_options(username, vstatus.mk)
359 vstatus.cur_selection = ''
361 elif vstatus.cur_selection[1] == '8':
362 v.display('GOT COKE?')
363 if ((os.system('su - "%s" -c "dispense %s"'%(username, vstatus.cur_selection[0])) >> 8) != 0):
364 v.display('SEEMS NOT')
366 v.display('GOT COKE!')
368 v.display(vstatus.cur_selection+' - $1.00')
369 if ((os.system('su - "%s" -c "dispense snack"'%(username)) >> 8) == 0):
370 v.vend(vstatus.cur_selection)
371 v.display('THANK YOU')
373 v.display('NO MONEY?')
375 vstatus.cur_selection = ''
376 vstatus.time_to_autologout = time() + 8
377 vstatus.last_timeout_refresh = None
380 def run_forever(rfh, wfh, options, cf):
381 v = VendingMachine(rfh, wfh)
382 vstatus = VendState(v)
384 logging.debug('PING is ' + str(v.ping()))
386 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
388 vstatus.mk.set_message(GREETING)
392 # This main loop is hideous and the work of the devil - mtearle
395 # notes for later surgery
396 # (event, counter, ' ')
406 except DispenseDatabaseException, e:
407 logging.error('Database error: '+str(e))
409 if vstatus.time_to_autologout != None:
410 time_left = vstatus.time_to_autologout - time()
411 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
412 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
413 vstatus.last_timeout_refresh = int(time_left)
414 vstatus.cur_selection = ''
416 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
417 vstatus.time_to_autologout = None
418 vstatus.cur_user = ''
420 vstatus.cur_selection = ''
421 vstatus.mk.set_message(GREETING)
423 if vstatus.time_to_autologout and not vstatus.mk.done(): vstatus.time_to_autologout = None
424 if vstatus.cur_user == '' and vstatus.time_to_autologout: vstatus.time_to_autologout = None
425 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
427 vstatus.time_to_autologout = time() + 15
428 vstatus.last_timeout_refresh = None
430 if vstatus.time_to_idle == None and vstatus.cur_user == '':
431 vstatus.time_to_idle = time() + 5
433 if vstatus.time_to_idle is not None and vstatus.cur_user != '': vstatus.time_to_idle = None
434 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle: idle_step()
435 if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle + 300:
436 vstatus.time_to_idle = time()
439 vstatus.mk.update_display()
443 e = v.next_event(0.05)
446 vstatus.time_to_idle = None
448 logging.debug('Got event: ' + repr(e))
451 handle_door_event(event, params, v, vstatus)
452 elif event == SWITCH:
453 handle_switch_event(event, params, v, vstatus)
455 handle_key_event(event, params, v, vstatus)
457 def connect_to_vend(options, cf):
460 logging.info('Connecting to vending machine using LAT')
461 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
462 rfh, wfh = latclient.get_fh()
463 elif options.use_serial:
464 # Open vending machine via serial.
465 logging.info('Connecting to vending machine using serial')
466 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
467 rfh,wfh = serialclient.get_fh()
469 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
470 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
472 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
473 sock.connect((options.host, options.port))
474 rfh = sock.makefile('r')
475 wfh = sock.makefile('w')
480 from optparse import OptionParser
482 op = OptionParser(usage="%prog [OPTION]...")
483 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')
484 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
485 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
486 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
487 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
488 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
489 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
490 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
491 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
492 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
493 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
494 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
495 options, args = op.parse_args()
498 op.error('extra command line arguments: ' + ' '.join(args))
503 'DBServer': ('Database', 'Server'),
504 'DBName': ('Database', 'Name'),
505 'DBUser': ('VendingMachine', 'DBUser'),
506 'DBPassword': ('VendingMachine', 'DBPassword'),
508 'ServiceName': ('VendingMachine', 'ServiceName'),
509 'ServicePassword': ('VendingMachine', 'Password'),
511 'ServerName': ('DecServer', 'Name'),
512 'ConnectPassword': ('DecServer', 'ConnectPassword'),
513 'PrivPassword': ('DecServer', 'PrivPassword'),
516 class VendConfigFile:
517 def __init__(self, config_file, options):
519 cp = ConfigParser.ConfigParser()
522 for option in options:
523 section, name = options[option]
524 value = cp.get(section, name)
525 self.__dict__[option] = value
527 except ConfigParser.Error, e:
528 raise SystemExit("Error reading config file "+config_file+": " + str(e))
530 def create_pid_file(name):
532 pid_file = file(name, 'w')
533 pid_file.write('%d\n'%os.getpid())
536 logging.warning('unable to write to pid file '+name+': '+str(e))
539 def do_nothing(signum, stack):
540 signal.signal(signum, do_nothing)
541 def stop_server(signum, stack): raise KeyboardInterrupt
542 signal.signal(signal.SIGHUP, do_nothing)
543 signal.signal(signal.SIGTERM, stop_server)
544 signal.signal(signal.SIGINT, stop_server)
546 options = parse_args()
547 config_opts = VendConfigFile(options.config_file, config_options)
548 if options.daemon: become_daemon()
549 set_up_logging(options)
550 if options.pid_file != '': create_pid_file(options.pid_file)
552 return options, config_opts
554 def clean_up_nicely(options, config_opts):
555 if options.pid_file != '':
557 os.unlink(options.pid_file)
558 logging.debug('Removed pid file '+options.pid_file)
559 except OSError: pass # if we can't delete it, meh
561 def set_up_logging(options):
562 logger = logging.getLogger()
564 if not options.daemon:
565 stderr_logger = logging.StreamHandler(sys.stderr)
566 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
567 logger.addHandler(stderr_logger)
569 if options.log_file != '':
571 file_logger = logging.FileHandler(options.log_file)
572 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
573 logger.addHandler(file_logger)
575 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
577 if options.syslog != None:
578 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
579 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
580 logger.addHandler(sys_logger)
583 logger.setLevel(logging.WARNING)
584 elif options.verbose:
585 logger.setLevel(logging.DEBUG)
587 logger.setLevel(logging.INFO)
590 dev_null = file('/dev/null')
591 fd = dev_null.fileno()
600 raise SystemExit('failed to fork: '+str(e))
602 def do_vend_server(options, config_opts):
605 rfh, wfh = connect_to_vend(options, config_opts)
606 except (SerialClientException, socket.error), e:
607 (exc_type, exc_value, exc_traceback) = sys.exc_info()
609 logging.error("Connection error: "+str(exc_type)+" "+str(e))
610 logging.info("Trying again in 5 seconds.")
615 run_forever(rfh, wfh, options, config_opts)
616 except VendingException:
617 logging.error("Connection died, trying again...")
618 logging.info("Trying again in 5 seconds.")
621 if __name__ == '__main__':
622 options, config_opts = set_stuff_up()
625 logging.warning('Starting Vend Server')
626 do_vend_server(options, config_opts)
627 logging.error('Vend Server finished unexpectedly, restarting')
628 except KeyboardInterrupt:
629 logging.info("Killed by signal, cleaning up")
630 clean_up_nicely(options, config_opts)
631 logging.warning("Vend Server stopped")
636 (exc_type, exc_value, exc_traceback) = sys.exc_info()
637 tb = format_tb(exc_traceback, 20)
640 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
641 logging.critical("Message: " + str(exc_value))
642 logging.critical("Traceback:")
644 for line in event.split('\n'):
645 logging.critical(' '+line)
646 logging.critical("This message should be considered a bug in the Vend Server.")
647 logging.critical("Please report this to someone who can fix it.")
649 logging.warning("Trying again anyway (might not help, but hey...)")