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):
427 # Open vending machine via serial.
428 logging.info('Connecting to vending machine using serial')
429 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
430 return serialclient.get_fh()
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()
437 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
438 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
440 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
441 sock.connect((options.host, options.port))
442 rfh = sock.makefile('r')
443 wfh = sock.makefile('w')
448 from optparse import OptionParser
450 op = OptionParser(usage="%prog [OPTION]...")
451 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')
452 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
453 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
454 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
455 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
456 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
457 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
458 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
459 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
460 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
461 options, args = op.parse_args()
464 op.error('extra command line arguments: ' + ' '.join(args))
469 'DBServer': ('Database', 'Server'),
470 'DBName': ('Database', 'Name'),
471 'DBUser': ('VendingMachine', 'DBUser'),
472 'DBPassword': ('VendingMachine', 'DBPassword'),
474 'ServiceName': ('VendingMachine', 'ServiceName'),
475 'ServicePassword': ('VendingMachine', 'Password'),
477 'ServerName': ('DecServer', 'Name'),
478 'ConnectPassword': ('DecServer', 'ConnectPassword'),
479 'PrivPassword': ('DecServer', 'PrivPassword'),
482 class VendConfigFile:
483 def __init__(self, config_file, options):
485 cp = ConfigParser.ConfigParser()
488 for option in options:
489 section, name = options[option]
490 value = cp.get(section, name)
491 self.__dict__[option] = value
493 except ConfigParser.Error, e:
494 raise SystemExit("Error reading config file "+config_file+": " + str(e))
496 def create_pid_file(name):
498 pid_file = file(name, 'w')
499 pid_file.write('%d\n'%os.getpid())
502 logging.warning('unable to write to pid file '+name+': '+str(e))
505 def do_nothing(signum, stack):
506 signal.signal(signum, do_nothing)
507 def stop_server(signum, stack): raise KeyboardInterrupt
508 signal.signal(signal.SIGHUP, do_nothing)
509 signal.signal(signal.SIGTERM, stop_server)
510 signal.signal(signal.SIGINT, stop_server)
512 options = parse_args()
513 config_opts = VendConfigFile(options.config_file, config_options)
514 if options.daemon: become_daemon()
515 set_up_logging(options)
516 if options.pid_file != '': create_pid_file(options.pid_file)
518 return options, config_opts
520 def clean_up_nicely(options, config_opts):
521 if options.pid_file != '':
523 os.unlink(options.pid_file)
524 logging.debug('Removed pid file '+options.pid_file)
525 except OSError: pass # if we can't delete it, meh
527 def set_up_logging(options):
528 logger = logging.getLogger()
530 if not options.daemon:
531 stderr_logger = logging.StreamHandler(sys.stderr)
532 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
533 logger.addHandler(stderr_logger)
535 if options.log_file != '':
537 file_logger = logging.FileHandler(options.log_file)
538 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
539 logger.addHandler(file_logger)
541 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
543 if options.syslog != None:
544 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
545 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
546 logger.addHandler(sys_logger)
549 logger.setLevel(logging.WARNING)
550 elif options.verbose:
551 logger.setLevel(logging.DEBUG)
553 logger.setLevel(logging.INFO)
556 dev_null = file('/dev/null')
557 fd = dev_null.fileno()
566 raise SystemExit('failed to fork: '+str(e))
568 def do_vend_server(options, config_opts):
571 rfh, wfh = connect_to_vend(options, config_opts)
572 except (SerialClientException, socket.error), e:
573 (exc_type, exc_value, exc_traceback) = sys.exc_info()
575 logging.error("Connection error: "+str(exc_type)+" "+str(e))
576 logging.info("Trying again in 5 seconds.")
581 run_forever(rfh, wfh, options, config_opts)
582 except VendingException:
583 logging.error("Connection died, trying again...")
584 logging.info("Trying again in 5 seconds.")
587 if __name__ == '__main__':
588 options, config_opts = set_stuff_up()
591 logging.warning('Starting Vend Server')
592 do_vend_server(options, config_opts)
593 logging.error('Vend Server finished unexpectedly, restarting')
594 except KeyboardInterrupt:
595 logging.info("Killed by signal, cleaning up")
596 clean_up_nicely(options, config_opts)
597 logging.warning("Vend Server stopped")
602 (exc_type, exc_value, exc_traceback) = sys.exc_info()
603 tb = format_tb(exc_traceback, 20)
606 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
607 logging.critical("Message: " + str(exc_value))
608 logging.critical("Traceback:")
610 for line in event.split('\n'):
611 logging.critical(' '+line)
612 logging.critical("This message should be considered a bug in the Vend Server.")
613 logging.critical("Please report this to someone who can fix it.")
615 logging.warning("Trying again anyway (might not help, but hey...)")