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 VendingMachine import VendingMachine, VendingException
15 from MessageKeeper import MessageKeeper
16 from HorizScroll import HorizScroll
17 from random import random, seed
18 from Idler import TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler
20 from posix import geteuid
23 This vending machine software brought to you by:
28 and a collective of hungry alpacas.
32 For a good time call +61 8 6488 3901
38 GREETING = 'UCC SNACKS'
45 class DispenseDatabaseException(Exception): pass
47 class DispenseDatabase:
48 def __init__(self, vending_machine, host, name, user, password):
49 self.vending_machine = vending_machine
50 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
51 self.db.query('LISTEN vend_requests')
53 def process_requests(self):
54 logging.debug('database processing')
55 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
57 outstanding = self.db.query(query).getresult()
58 except (pg.error,), db_err:
59 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
60 for (id, slot) in outstanding:
61 (worked, code, string) = self.vending_machine.vend(slot)
62 logging.debug (str((worked, code, string)))
64 query = 'SELECT vend_success(%s)'%id
65 self.db.query(query).getresult()
67 query = 'SELECT vend_failed(%s)'%id
68 self.db.query(query).getresult()
70 def handle_events(self):
71 notifier = self.db.getnotify()
72 while notifier is not None:
73 self.process_requests()
74 notify = self.db.getnotify()
76 def scroll_options(username, mk, welcome = False):
78 msg = [(center('WELCOME'), False, 0.8),
79 (center(username), False, 0.8)]
82 choices = ' '*10+'CHOICES: '
84 coke_machine = file('/home/other/coke/coke_contents')
85 cokes = coke_machine.readlines()
92 (slot_num, price, slot_name) = c.split(' ', 2)
93 if slot_name == 'dead': continue
94 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
96 choices += 'OR A SNACK. '
97 choices += '99 TO READ AGAIN. '
99 msg.append((choices, False, None))
104 info = pwd.getpwuid(uid)
106 logging.info('getting pin for uid %d: user not in password file'%uid)
108 if info.pw_dir == None: return False
109 pinfile = os.path.join(info.pw_dir, '.pin')
113 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
116 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
121 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
123 pinstr = f.readline()
125 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
126 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
130 def has_good_pin(uid):
131 return get_pin(uid) != None
133 def verify_user_pin(uid, pin):
134 if get_pin(uid) == pin:
135 info = pwd.getpwuid(uid)
136 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
139 logging.info('refused pin for uid %d'%(uid))
142 def door_open_mode(v):
143 logging.warning("Entering open door mode")
144 v.display("-FEED ME-")
150 if params == 1: # door closed
151 logging.warning('Leaving open door mode')
152 v.display("-YUM YUM!-")
158 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
159 choice = int(random()*len(messages))
160 msg = messages[choice]
161 left = range(len(msg))
162 for i in range(len(msg)):
163 if msg[i] == ' ': left.remove(i)
167 for i in range(0, len(msg)):
173 s += chr(int(random()*26)+ord('A'))
182 return ' '*((LEN-len(str))/2)+str
192 StringIdler(v, text="Kill 'em all", repeat=False),
193 StringIdler(v, text=CREDITS),
194 StringIdler(v, text=str(math.pi) + " "),
195 StringIdler(v, text=str(math.e) + " "),
201 GrayIdler(v,one="*",zero="-"),
202 GrayIdler(v,one="/",zero="\\"),
203 GrayIdler(v,one="X",zero="O"),
204 GrayIdler(v,one="*",zero="-",reorder=1),
205 GrayIdler(v,one="/",zero="\\",reorder=1),
206 GrayIdler(v,one="X",zero="O",reorder=1),
208 idler = choose_idler()
212 idler = idlers[int(random()*len(idlers))]
221 def run_forever(rfh, wfh, options, cf):
222 v = VendingMachine(rfh, wfh)
223 logging.debug('PING is ' + str(v.ping()))
225 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
230 mk = MessageKeeper(v)
231 mk.set_message(GREETING)
232 time_to_autologout = None
235 last_timeout_refresh = None
241 except DispenseDatabaseException, e:
242 logging.error('Database error: '+str(e))
244 if time_to_autologout != None:
245 time_left = time_to_autologout - time()
246 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
247 mk.set_message('LOGOUT: '+str(int(time_left)))
248 last_timeout_refresh = int(time_left)
251 if time_to_autologout != None and time_to_autologout - time() <= 0:
252 time_to_autologout = None
256 mk.set_message(GREETING)
258 if time_to_autologout and not mk.done(): time_to_autologout = None
259 if cur_user == '' and time_to_autologout: time_to_autologout = None
260 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
262 time_to_autologout = time() + 15
264 if time_to_idle == None and cur_user == '':
265 time_to_idle = time() + 5
267 if time_to_idle is not None and cur_user != '': time_to_idle = None
268 if time_to_idle is not None and time() > time_to_idle: idle_step()
269 if time_to_idle is not None and time() > time_to_idle + 300:
270 time_to_idle = time()
277 e = v.next_event(0.05)
282 logging.debug('Got event: ' + repr(e))
288 mk.set_message(GREETING)
289 elif event == SWITCH:
290 # don't care right now.
294 # complicated key handling here:
295 if len(cur_user) < 5:
298 mk.set_message(GREETING)
300 cur_user += chr(key + ord('0'))
301 mk.set_message('UID: '+cur_user)
302 if len(cur_user) == 5:
304 if not has_good_pin(uid):
305 logging.info('user '+cur_user+' has a bad PIN')
307 #[(center('INVALID'), False, 0.7),
308 #(center('PIN'), False, 0.7),
309 #(center('SETUP'), False, 1.0),
310 #(GREETING, False, None)])
312 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
313 (GREETING, False, None)])
318 mk.set_message('PIN: ')
319 logging.info('need pin for user %s'%cur_user)
321 elif len(cur_pin) < PIN_LENGTH:
325 mk.set_message(GREETING)
328 mk.set_message('PIN: ')
330 cur_pin += chr(key + ord('0'))
331 mk.set_message('PIN: '+'X'*len(cur_pin))
332 if len(cur_pin) == PIN_LENGTH:
333 username = verify_user_pin(int(cur_user), int(cur_pin))
337 scroll_options(username, mk, True)
342 [(center('BAD PIN'), False, 1.0),
343 (center('SORRY'), False, 0.5),
344 (GREETING, False, None)])
348 elif len(cur_selection) == 0:
354 [(center('BYE!'), False, 1.5),
355 (GREETING, False, None)])
357 cur_selection += chr(key + ord('0'))
358 mk.set_message('SELECT: '+cur_selection)
359 time_to_autologout = None
360 elif len(cur_selection) == 1:
363 time_to_autologout = None
364 scroll_options(username, mk)
367 cur_selection += chr(key + ord('0'))
368 #make_selection(cur_selection)
369 # XXX this should move somewhere else:
370 if cur_selection == '55':
371 mk.set_message('OPENSESAME')
372 logging.info('dispensing a door for %s'%username)
374 ret = os.system('su - "%s" -c "dispense door"'%username)
376 ret = os.system('dispense door')
378 logging.info('door opened')
379 mk.set_message(center('DOOR OPEN'))
381 logging.warning('user %s tried to dispense a bad door'%username)
382 mk.set_message(center('BAD DOOR'))
384 elif cur_selection == '91':
386 elif cur_selection == '99':
387 scroll_options(username, mk)
390 elif cur_selection[1] == '8':
391 v.display('GOT COKE?')
392 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
394 v.display('HERES A '+cur_selection)
395 v.vend(cur_selection)
397 v.display('THANK YOU')
400 time_to_autologout = time() + 8
402 def connect_to_vend(options, cf):
403 # Open vending machine via LAT?
405 logging.info('Connecting to vending machine using LAT')
406 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
407 rfh, wfh = latclient.get_fh()
409 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
410 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
412 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
413 sock.connect((options.host, options.port))
414 rfh = sock.makefile('r')
415 wfh = sock.makefile('w')
420 from optparse import OptionParser
422 op = OptionParser(usage="%prog [OPTION]...")
423 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')
424 op.add_option('--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
425 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
426 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
427 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
428 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
429 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
430 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
431 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
432 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
433 options, args = op.parse_args()
436 op.error('extra command line arguments: ' + ' '.join(args))
441 'DBServer': ('Database', 'Server'),
442 'DBName': ('Database', 'Name'),
443 'DBUser': ('VendingMachine', 'DBUser'),
444 'DBPassword': ('VendingMachine', 'DBPassword'),
446 'ServiceName': ('VendingMachine', 'ServiceName'),
447 'ServicePassword': ('VendingMachine', 'Password'),
449 'ServerName': ('DecServer', 'Name'),
450 'ConnectPassword': ('DecServer', 'ConnectPassword'),
451 'PrivPassword': ('DecServer', 'PrivPassword'),
454 class VendConfigFile:
455 def __init__(self, config_file, options):
457 cp = ConfigParser.ConfigParser()
460 for option in options:
461 section, name = options[option]
462 value = cp.get(section, name)
463 self.__dict__[option] = value
465 except ConfigParser.Error, e:
466 raise SystemExit("Error reading config file "+config_file+": " + str(e))
468 def create_pid_file(name):
470 pid_file = file(name, 'w')
471 pid_file.write('%d\n'%os.getpid())
474 logging.warning('unable to write to pid file '+name+': '+str(e))
477 def do_nothing(signum, stack):
478 signal.signal(signum, do_nothing)
479 def stop_server(signum, stack): raise KeyboardInterrupt
480 signal.signal(signal.SIGHUP, do_nothing)
481 signal.signal(signal.SIGTERM, stop_server)
482 signal.signal(signal.SIGINT, stop_server)
484 options = parse_args()
485 config_opts = VendConfigFile(options.config_file, config_options)
486 if options.daemon: become_daemon()
487 set_up_logging(options)
488 if options.pid_file != '': create_pid_file(options.pid_file)
490 return options, config_opts
492 def clean_up_nicely(options, config_opts):
493 if options.pid_file != '':
495 os.unlink(options.pid_file)
496 logging.debug('Removed pid file '+options.pid_file)
497 except OSError: pass # if we can't delete it, meh
499 def set_up_logging(options):
500 logger = logging.getLogger()
502 if not options.daemon:
503 stderr_logger = logging.StreamHandler(sys.stderr)
504 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
505 logger.addHandler(stderr_logger)
507 if options.log_file != '':
509 file_logger = logging.FileHandler(options.log_file)
510 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
511 logger.addHandler(file_logger)
513 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
515 if options.syslog != None:
516 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
517 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
518 logger.addHandler(sys_logger)
521 logger.setLevel(logging.WARNING)
522 elif options.verbose:
523 logger.setLevel(logging.DEBUG)
525 logger.setLevel(logging.INFO)
528 dev_null = file('/dev/null')
529 fd = dev_null.fileno()
538 raise SystemExit('failed to fork: '+str(e))
540 def do_vend_server(options, config_opts):
543 rfh, wfh = connect_to_vend(options, config_opts)
544 except (LATClientException, socket.error), e:
545 (exc_type, exc_value, exc_traceback) = sys.exc_info()
547 logging.error("Connection error: "+str(exc_type)+" "+str(e))
548 logging.info("Trying again in 5 seconds.")
553 run_forever(rfh, wfh, options, config_opts)
554 except VendingException:
555 logging.error("Connection died, trying again...")
556 logging.info("Trying again in 5 seconds.")
559 if __name__ == '__main__':
560 options, config_opts = set_stuff_up()
563 logging.warning('Starting Vend Server')
564 do_vend_server(options, config_opts)
565 logging.error('Vend Server finished unexpectedly, restarting')
566 except KeyboardInterrupt:
567 logging.info("Killed by signal, cleaning up")
568 clean_up_nicely(options, config_opts)
569 logging.warning("Vend Server stopped")
574 (exc_type, exc_value, exc_traceback) = sys.exc_info()
575 tb = format_tb(exc_traceback, 20)
578 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
579 logging.critical("Message: " + str(exc_value))
580 logging.critical("Traceback:")
582 for line in event.split('\n'):
583 logging.critical(' '+line)
584 logging.critical("This message should be considered a bug in the Vend Server.")
585 logging.critical("Please report this to someone who can fix it.")
587 logging.warning("Trying again anyway (might not help, but hey...)")