7 import sys, os, string, re, pwd, signal, math, syslog
8 import logging, logging.handlers
9 from traceback import format_tb
11 from time import time, sleep, mktime, localtime
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 GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
20 from SnackConfig import get_snacks, get_snack
22 from posix import geteuid
25 This vending machine software brought to you by:
30 and a collective of hungry alpacas.
34 For a good time call +61 8 6488 3901
55 STATE_GRANDFATHER_CLOCK,
61 class DispenseDatabaseException(Exception): pass
63 class DispenseDatabase:
64 def __init__(self, vending_machine, host, name, user, password):
65 self.vending_machine = vending_machine
66 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
67 self.db.query('LISTEN vend_requests')
69 def process_requests(self):
70 logging.debug('database processing')
71 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
73 outstanding = self.db.query(query).getresult()
74 except (pg.error,), db_err:
75 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
76 for (id, slot) in outstanding:
77 (worked, code, string) = self.vending_machine.vend(slot)
78 logging.debug (str((worked, code, string)))
80 query = 'SELECT vend_success(%s)'%id
81 self.db.query(query).getresult()
83 query = 'SELECT vend_failed(%s)'%id
84 self.db.query(query).getresult()
86 def handle_events(self):
87 notifier = self.db.getnotify()
88 while notifier is not None:
89 self.process_requests()
90 notify = self.db.getnotify()
92 def scroll_options(username, mk, welcome = False):
94 msg = [(center('WELCOME'), False, TEXT_SPEED),
95 (center(username), False, TEXT_SPEED)]
98 choices = ' '*10+'CHOICES: '
100 coke_machine = file('/home/other/coke/coke_contents')
101 cokes = coke_machine.readlines()
108 (slot_num, price, slot_name) = c.split(' ', 2)
109 if slot_name == 'dead': continue
110 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
112 # we don't want to print snacks for now since it'll be too large
113 # and there's physical bits of paper in the machine anyway - matt
115 # snacks = get_snacks()
119 # for slot, ( name, price ) in snacks.items():
120 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
122 choices += '55-DOOR '
123 choices += 'OR ANOTHER SNACK. '
124 choices += '99 TO READ AGAIN. '
125 choices += 'CHOICE? '
126 msg.append((choices, False, None))
131 info = pwd.getpwuid(uid)
133 logging.info('getting pin for uid %d: user not in password file'%uid)
135 if info.pw_dir == None: return False
136 pinfile = os.path.join(info.pw_dir, '.pin')
140 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
143 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
144 os.chmod(pinfile, 0600)
148 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
150 pinstr = f.readline()
152 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
153 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
157 def has_good_pin(uid):
158 return get_pin(uid) != None
160 def verify_user_pin(uid, pin):
161 if get_pin(uid) == pin:
162 info = pwd.getpwuid(uid)
163 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
166 logging.info('refused pin for uid %d'%(uid))
172 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
173 choice = int(random()*len(messages))
174 msg = messages[choice]
175 left = range(len(msg))
176 for i in range(len(msg)):
177 if msg[i] == ' ': left.remove(i)
181 for i in range(0, len(msg)):
187 s += chr(int(random()*26)+ord('A'))
196 return ' '*((LEN-len(str))/2)+str
207 StringIdler(v, text="Kill 'em all", repeat=False),
208 GrayIdler(v,one="*",zero="-"),
209 StringIdler(v, text=CREDITS),
210 GrayIdler(v,one="/",zero="\\"),
212 GrayIdler(v,one="X",zero="O"),
213 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
214 GrayIdler(v,one="*",zero="-",reorder=1),
215 StringIdler(v, text=str(math.pi) + " "),
217 GrayIdler(v,one="/",zero="\\",reorder=1),
218 StringIdler(v, text=str(math.e) + " "),
219 GrayIdler(v,one="X",zero="O",reorder=1),
220 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
221 PipeIdler(v, "/usr/bin/getent", "passwd"),
230 def reset_idler(v, vstatus, t = None):
232 idler = GreetingIdler(v, t)
233 vstatus.time_of_next_idlestep = time()+idler.next()
234 vstatus.time_of_next_idler = None
235 vstatus.time_to_autologout = None
236 vstatus.change_state(STATE_IDLE, 1)
241 average_affinity = 10 # guessing here...
243 if idler and idler.__class__ != GreetingIdler:
244 iiindex = idlers.index(idler)
248 move = int(random()*len(idlers)*average_affinity) + 1
253 idler = idlers[iiindex]
254 move -= idler.affinity()
258 def idle_step(vstatus):
262 vstatus.time_of_next_idler = time() + 30
263 nextidle = idler.next()
265 nextidle = IDLE_SPEED
266 vstatus.time_of_next_idlestep = time()+nextidle
269 def __init__(self,v):
270 self.state_table = {}
271 self.state = STATE_IDLE
274 self.mk = MessageKeeper(v)
278 self.cur_selection = ''
279 self.time_to_autologout = None
281 self.last_timeout_refresh = None
283 def change_state(self,newstate,newcounter=None):
284 if self.state != newstate:
285 #print "Changing state from: ",
289 self.state = newstate
291 if newcounter is not None and self.counter != newcounter:
292 #print "Changing counter from: ",
296 self.counter = newcounter
300 def handle_tick_event(event, params, v, vstatus):
301 # don't care right now.
304 def handle_switch_event(event, params, v, vstatus):
305 # don't care right now.
309 def do_nothing(state, event, params, v, vstatus):
310 print "doing nothing (s,e,p)", state, " ", event, " ", params
313 def handle_getting_uid_idle(state, event, params, v, vstatus):
314 # don't care right now.
317 def handle_getting_pin_idle(state, event, params, v, vstatus):
318 # don't care right now.
321 def handle_get_selection_idle(state, event, params, v, vstatus):
322 # don't care right now.
324 ### State logging out ..
325 if vstatus.time_to_autologout != None:
326 time_left = vstatus.time_to_autologout - time()
327 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
328 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
329 vstatus.last_timeout_refresh = int(time_left)
330 vstatus.cur_selection = ''
332 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
333 vstatus.time_to_autologout = None
334 vstatus.cur_user = ''
336 vstatus.cur_selection = ''
338 reset_idler(v, vstatus)
340 ### State fully logged out ... reset variables
341 if vstatus.time_to_autologout and not vstatus.mk.done():
342 vstatus.time_to_autologout = None
343 if vstatus.cur_user == '' and vstatus.time_to_autologout:
344 vstatus.time_to_autologout = None
347 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
349 vstatus.time_to_autologout = time() + 15
350 vstatus.last_timeout_refresh = None
352 ## FIXME - this may need to be elsewhere.....
354 vstatus.mk.update_display()
358 def handle_get_selection_key(state, event, params, v, vstatus):
360 if len(vstatus.cur_selection) == 0:
363 vstatus.cur_user = ''
364 vstatus.cur_selection = ''
366 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
367 reset_idler(v, vstatus, 2)
369 vstatus.cur_selection += chr(key + ord('0'))
370 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
371 vstatus.time_to_autologout = None
372 elif len(vstatus.cur_selection) == 1:
374 vstatus.cur_selection = ''
375 vstatus.time_to_autologout = None
376 scroll_options(vstatus.username, vstatus.mk)
379 vstatus.cur_selection += chr(key + ord('0'))
381 make_selection(v,vstatus)
382 vstatus.cur_selection = ''
383 vstatus.time_to_autologout = time() + 8
384 vstatus.last_timeout_refresh = None
387 price_check(v,vstatus)
388 vstatus.cur_selection = ''
389 vstatus.time_to_autologout = None
390 vstatus.last_timeout_refresh = None
392 def make_selection(v, vstatus):
393 # should use sudo here
394 if vstatus.cur_selection == '55':
395 vstatus.mk.set_message('OPENSESAME')
396 logging.info('dispensing a door for %s'%vstatus.username)
398 ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
400 ret = os.system('dispense door')
402 logging.info('door opened')
403 vstatus.mk.set_message(center('DOOR OPEN'))
405 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
406 vstatus.mk.set_message(center('BAD DOOR'))
408 elif vstatus.cur_selection == '81':
410 elif vstatus.cur_selection == '99':
411 scroll_options(vstatus.username, vstatus.mk)
412 vstatus.cur_selection = ''
414 elif vstatus.cur_selection[1] == '8':
415 v.display('GOT DRINK?')
416 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
417 v.display('SEEMS NOT')
419 v.display('GOT DRINK!')
421 # first see if it's a named slot
423 price, shortname, name = get_snack( vstatus.cur_selection )
425 price, shortname, name = get_snack( '--' )
426 dollarprice = "$%.2f" % ( price / 100.0 )
427 v.display(vstatus.cur_selection+' - %s'%dollarprice)
428 exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
430 # magic dispense syslog service
431 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
432 v.vend(vstatus.cur_selection)
433 v.display('THANK YOU')
435 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
436 v.display('NO MONEY?')
440 def price_check(v, vstatus):
441 if vstatus.cur_selection[1] == '8':
442 v.display(center('SEE COKE'))
444 # first see if it's a named slot
446 price, shortname, name = get_snack( vstatus.cur_selection )
448 price, shortname, name = get_snack( '--' )
449 dollarprice = "$%.2f" % ( price / 100.0 )
450 v.display(vstatus.cur_selection+' - %s'%dollarprice)
453 def handle_getting_pin_key(state, event, params, v, vstatus):
454 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
456 if len(vstatus.cur_pin) < PIN_LENGTH:
458 if vstatus.cur_pin == '':
459 vstatus.cur_user = ''
460 reset_idler(v, vstatus)
464 vstatus.mk.set_message('PIN: ')
466 vstatus.cur_pin += chr(key + ord('0'))
467 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
468 if len(vstatus.cur_pin) == PIN_LENGTH:
469 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
472 vstatus.cur_selection = ''
473 vstatus.change_state(STATE_GET_SELECTION)
474 scroll_options(vstatus.username, vstatus.mk, True)
478 vstatus.mk.set_messages(
479 [(center('BAD PIN'), False, 1.0),
480 (center('SORRY'), False, 0.5)])
481 vstatus.cur_user = ''
484 reset_idler(v, vstatus, 2)
489 def handle_getting_uid_key(state, event, params, v, vstatus):
490 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
493 # complicated key handling here:
495 if len(vstatus.cur_user) == 0 and key == 9:
496 vstatus.cur_selection = ''
497 vstatus.time_to_autologout = None
498 vstatus.mk.set_message('PRICECHECK')
500 scroll_options('', vstatus.mk)
501 vstatus.change_state(STATE_GET_SELECTION)
504 if len(vstatus.cur_user) <8:
506 vstatus.cur_user = ''
508 reset_idler(v, vstatus)
510 vstatus.cur_user += chr(key + ord('0'))
511 #logging.info('dob: '+vstatus.cur_user)
512 if len(vstatus.cur_user) > 5:
513 vstatus.mk.set_message('>'+vstatus.cur_user)
515 vstatus.mk.set_message('UID: '+vstatus.cur_user)
518 # Easter egg for nikita's birthday -- DGB
519 if len(vstatus.cur_user) == 8:
520 if vstatus.cur_user != "07051980":
521 vstatus.mk.set_messages(
522 [(' '*9+'ONE MORE TRY NiKiTa'+' '*10, False, 3)])
523 vstatus.cur_user = ''
524 reset_idler(v, vstatus, 3)
528 vstatus.mk.set_messages(
529 [(center(' GUILD MAILBOX NUMBER 64 '), False, 20),
530 (center(' GUILD MAILBOX NUMBER 64 '), False, 20),
531 (center(' GUILD MAILBOX NUMBER 64 '), False, 20),
532 (center(' GUILD MAILBOX NUMBER 64 '), False, 20)])
535 vstatus.cur_user = ''
537 #reset_idler(v, vstatus, 10)
538 reset_idler(v, vstatus, 2)
540 # End easter egg part 1
541 if len(vstatus.cur_user) == 5:
542 uid = int(vstatus.cur_user)
544 # Easter egg for nikita's birthday -- DGB
545 if vstatus.cur_user == '07051':
547 vstatus.cur_user = ''
548 reset_idler(v, vstatus)
550 # vstatus.cur_user += chr(key + ord('0'))
551 logging.info(' == 5 dob: '+vstatus.cur_user)
552 vstatus.mk.set_message('>'+vstatus.cur_user)
554 # end easter egg part 2
557 logging.info('user '+vstatus.cur_user+' has a bad PIN')
563 Welcome to Picklevision Sytems, Sunnyvale, CA
565 Greetings Professor Falken.
570 Shall we play a game?
573 Please choose from the following menu:
580 6. Toxic and Biochemical Warfare
581 7. Global Thermonuclear War
585 Wouldn't you prefer a nice game of chess?
587 """.replace('\n',' ')
588 vstatus.mk.set_messages([(pfalken, False, 10)])
589 vstatus.cur_user = ''
592 reset_idler(v, vstatus, 10)
596 if not has_good_pin(uid):
597 logging.info('user '+vstatus.cur_user+' has a bad PIN')
598 vstatus.mk.set_messages(
599 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
600 vstatus.cur_user = ''
603 reset_idler(v, vstatus, 3)
609 vstatus.mk.set_message('PIN: ')
610 logging.info('need pin for user %s'%vstatus.cur_user)
611 vstatus.change_state(STATE_GETTING_PIN)
615 def handle_idle_key(state, event, params, v, vstatus):
616 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
621 vstatus.cur_user = ''
622 reset_idler(v, vstatus)
625 vstatus.change_state(STATE_GETTING_UID)
626 run_handler(event, key, v, vstatus)
629 def handle_idle_tick(state, event, params, v, vstatus):
631 if vstatus.mk.done():
634 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
635 vstatus.time_of_next_idler = time() + 30
640 vstatus.mk.update_display()
642 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
643 run_handler(event, params, v, vstatus)
646 def beep_on(when, before=0):
647 start = int(when - before)
651 if now >= start and now <= end:
655 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
656 ### check for interesting times
659 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
660 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
661 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
662 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
664 hourfromnow = localtime(time() + 3600)
666 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
667 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
668 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
670 ## check for X seconds to the hour
671 ## if case, update counter to 2
672 if beep_on(onthehour,15) \
673 or beep_on(halfhour,0) \
674 or beep_on(quarterhour,0) \
675 or beep_on(threequarterhour,0) \
676 or beep_on(fivetothehour,0):
677 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
678 run_handler(event, params, v, vstatus)
680 vstatus.change_state(STATE_IDLE)
682 def handle_grandfather_tick(state, event, params, v, vstatus):
686 ### we live in interesting times
689 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
690 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
691 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
692 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
694 hourfromnow = localtime(time() + 3600)
696 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
697 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
698 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
701 #print "when it fashionable to wear a onion on your hip"
703 if beep_on(onthehour,15):
705 next_hour=((hourfromnow[3] + 11) % 12) + 1
706 if onthehour - time() < next_hour and onthehour - time() > 0:
711 msg.append(("DING!", False, None))
713 msg.append((" DING!", False, None))
714 elif int(onthehour - time()) == 0:
716 msg.append((" BONG!", False, None))
717 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
718 elif beep_on(halfhour,0):
721 msg.append((" HALFHOUR ", False, 50))
722 elif beep_on(quarterhour,0):
725 msg.append((" QTR HOUR ", False, 50))
726 elif beep_on(threequarterhour,0):
729 msg.append((" 3 QTR HR ", False, 50))
730 elif beep_on(fivetothehour,0):
733 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
737 ## check for X seconds to the hour
740 vstatus.mk.set_messages(msg)
743 vstatus.mk.update_display()
744 ## if no longer case, return to idle
746 ## change idler to be clock
747 if go_idle and vstatus.mk.done():
748 vstatus.change_state(STATE_IDLE,1)
750 def handle_door_idle(state, event, params, v, vstatus):
751 # don't care right now.
754 def handle_door_event(state, event, params, v, vstatus):
755 if params == 0: #door open
756 vstatus.change_state(STATE_DOOR_OPENING)
757 logging.warning("Entering open door mode")
758 v.display("-FEED ME-")
760 vstatus.cur_user = ''
762 elif params == 1: #door closed
763 vstatus.change_state(STATE_DOOR_CLOSING)
764 reset_idler(v, vstatus, 3)
766 logging.warning('Leaving open door mode')
767 v.display("-YUM YUM!-")
769 def return_to_idle(state,event,params,v,vstatus):
770 reset_idler(v, vstatus)
772 def create_state_table(vstatus):
773 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
774 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
775 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
777 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
778 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
779 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
781 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
782 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
783 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
785 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
786 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
787 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
789 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
790 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
791 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
793 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
794 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
795 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
797 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
798 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
799 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
800 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
801 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
802 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
804 def get_state_table_handler(vstatus, state, event, counter):
805 return vstatus.state_table[(state,event,counter)]
807 def time_to_next_update(vstatus):
808 idle_update = vstatus.time_of_next_idlestep - time()
809 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
810 mk_update = vstatus.mk.next_update - time()
811 if mk_update < idle_update:
812 idle_update = mk_update
815 def run_forever(rfh, wfh, options, cf):
816 v = VendingMachine(rfh, wfh)
817 vstatus = VendState(v)
818 create_state_table(vstatus)
820 logging.debug('PING is ' + str(v.ping()))
822 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
825 reset_idler(v, vstatus)
827 # This main loop was hideous and the work of the devil.
828 # This has now been fixed (mostly) - mtearle
831 # notes for later surgery
832 # (event, counter, ' ')
836 # ( return state - not currently implemented )
842 except DispenseDatabaseException, e:
843 logging.error('Database error: '+str(e))
846 timeout = time_to_next_update(vstatus)
847 e = v.next_event(timeout)
850 run_handler(event, params, v, vstatus)
852 # logging.debug('Got event: ' + repr(e))
855 def run_handler(event, params, v, vstatus):
856 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
858 handler(vstatus.state, event, params, v, vstatus)
860 def connect_to_vend(options, cf):
863 logging.info('Connecting to vending machine using LAT')
864 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
865 rfh, wfh = latclient.get_fh()
866 elif options.use_serial:
867 # Open vending machine via serial.
868 logging.info('Connecting to vending machine using serial')
869 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
870 rfh,wfh = serialclient.get_fh()
872 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
873 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
875 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
876 sock.connect((options.host, options.port))
877 rfh = sock.makefile('r')
878 wfh = sock.makefile('w')
883 from optparse import OptionParser
885 op = OptionParser(usage="%prog [OPTION]...")
886 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')
887 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
888 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
889 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
890 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
891 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
892 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
893 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
894 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
895 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
896 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
897 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
898 options, args = op.parse_args()
901 op.error('extra command line arguments: ' + ' '.join(args))
906 'DBServer': ('Database', 'Server'),
907 'DBName': ('Database', 'Name'),
908 'DBUser': ('VendingMachine', 'DBUser'),
909 'DBPassword': ('VendingMachine', 'DBPassword'),
911 'ServiceName': ('VendingMachine', 'ServiceName'),
912 'ServicePassword': ('VendingMachine', 'Password'),
914 'ServerName': ('DecServer', 'Name'),
915 'ConnectPassword': ('DecServer', 'ConnectPassword'),
916 'PrivPassword': ('DecServer', 'PrivPassword'),
919 class VendConfigFile:
920 def __init__(self, config_file, options):
922 cp = ConfigParser.ConfigParser()
925 for option in options:
926 section, name = options[option]
927 value = cp.get(section, name)
928 self.__dict__[option] = value
930 except ConfigParser.Error, e:
931 raise SystemExit("Error reading config file "+config_file+": " + str(e))
933 def create_pid_file(name):
935 pid_file = file(name, 'w')
936 pid_file.write('%d\n'%os.getpid())
939 logging.warning('unable to write to pid file '+name+': '+str(e))
942 def do_nothing(signum, stack):
943 signal.signal(signum, do_nothing)
944 def stop_server(signum, stack): raise KeyboardInterrupt
945 signal.signal(signal.SIGHUP, do_nothing)
946 signal.signal(signal.SIGTERM, stop_server)
947 signal.signal(signal.SIGINT, stop_server)
949 options = parse_args()
950 config_opts = VendConfigFile(options.config_file, config_options)
951 if options.daemon: become_daemon()
952 set_up_logging(options)
953 if options.pid_file != '': create_pid_file(options.pid_file)
955 return options, config_opts
957 def clean_up_nicely(options, config_opts):
958 if options.pid_file != '':
960 os.unlink(options.pid_file)
961 logging.debug('Removed pid file '+options.pid_file)
962 except OSError: pass # if we can't delete it, meh
964 def set_up_logging(options):
965 logger = logging.getLogger()
967 if not options.daemon:
968 stderr_logger = logging.StreamHandler(sys.stderr)
969 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
970 logger.addHandler(stderr_logger)
972 if options.log_file != '':
974 file_logger = logging.FileHandler(options.log_file)
975 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
976 logger.addHandler(file_logger)
978 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
980 if options.syslog != None:
981 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
982 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
983 logger.addHandler(sys_logger)
986 logger.setLevel(logging.WARNING)
987 elif options.verbose:
988 logger.setLevel(logging.DEBUG)
990 logger.setLevel(logging.INFO)
993 dev_null = file('/dev/null')
994 fd = dev_null.fileno()
1003 raise SystemExit('failed to fork: '+str(e))
1005 def do_vend_server(options, config_opts):
1008 rfh, wfh = connect_to_vend(options, config_opts)
1009 except (SerialClientException, socket.error), e:
1010 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1012 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1013 logging.info("Trying again in 5 seconds.")
1018 run_forever(rfh, wfh, options, config_opts)
1019 except VendingException:
1020 logging.error("Connection died, trying again...")
1021 logging.info("Trying again in 5 seconds.")
1024 if __name__ == '__main__':
1025 options, config_opts = set_stuff_up()
1028 logging.warning('Starting Vend Server')
1029 do_vend_server(options, config_opts)
1030 logging.error('Vend Server finished unexpectedly, restarting')
1031 except KeyboardInterrupt:
1032 logging.info("Killed by signal, cleaning up")
1033 clean_up_nicely(options, config_opts)
1034 logging.warning("Vend Server stopped")
1039 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1040 tb = format_tb(exc_traceback, 20)
1043 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1044 logging.critical("Message: " + str(exc_value))
1045 logging.critical("Traceback:")
1047 for line in event.split('\n'):
1048 logging.critical(' '+line)
1049 logging.critical("This message should be considered a bug in the Vend Server.")
1050 logging.critical("Please report this to someone who can fix it.")
1052 logging.warning("Trying again anyway (might not help, but hey...)")