document idlers and slow them down!
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5
6 import ConfigParser
7 import sys, os, string, re, pwd, signal, math
8 import logging, logging.handlers
9 from traceback import format_tb
10 if USE_DB: import pg
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 TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
20 import socket
21 from posix import geteuid
22
23 CREDITS="""
24 This vending machine software brought to you by:
25 Bernard Blackham
26 Mark Tearle
27 Nick Bannon
28 Cameron Patrick
29 and a collective of hungry alpacas.
30
31
32
33 For a good time call +61 8 6488 3901
34
35
36
37 """
38
39 GREETING = 'UCC SNACKS'
40 PIN_LENGTH = 4
41
42 DOOR = 1
43 SWITCH = 2
44 KEY = 3
45 TICK = 4
46
47
48 STATE_IDLE = 1
49 STATE_DOOR_OPENING = 2
50 STATE_DOOR_CLOSING = 3
51 STATE_GETTING_UID = 4
52 STATE_GETTING_PIN = 5
53 STATE_GET_SELECTION = 6
54 STATE_GRANDFATHER_CLOCK = 7
55
56 TEXT_SPEED = 0.8
57 IDLE_SPEED = 0.02
58
59 class DispenseDatabaseException(Exception): pass
60
61 class DispenseDatabase:
62         def __init__(self, vending_machine, host, name, user, password):
63                 self.vending_machine = vending_machine
64                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
65                 self.db.query('LISTEN vend_requests')
66
67         def process_requests(self):
68                 logging.debug('database processing')
69                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
70                 try:
71                         outstanding = self.db.query(query).getresult()
72                 except (pg.error,), db_err:
73                         raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
74                 for (id, slot) in outstanding:
75                         (worked, code, string) = self.vending_machine.vend(slot)
76                         logging.debug (str((worked, code, string)))
77                         if worked:
78                                 query = 'SELECT vend_success(%s)'%id
79                                 self.db.query(query).getresult()
80                         else:
81                                 query = 'SELECT vend_failed(%s)'%id
82                                 self.db.query(query).getresult()
83
84         def handle_events(self):
85                 notifier = self.db.getnotify()
86                 while notifier is not None:
87                         self.process_requests()
88                         notify = self.db.getnotify()
89
90 def scroll_options(username, mk, welcome = False):
91         if welcome:
92                 msg = [(center('WELCOME'), False, TEXT_SPEED),
93                            (center(username), False, TEXT_SPEED)]
94         else:
95                 msg = []
96         choices = ' '*10+'CHOICES: '
97         try:
98                 coke_machine = file('/home/other/coke/coke_contents')
99                 cokes = coke_machine.readlines()
100                 coke_machine.close()
101         except:
102                 cokes = []
103                 pass
104         for c in cokes:
105                 c = c.strip()
106                 (slot_num, price, slot_name) = c.split(' ', 2)
107                 if slot_name == 'dead': continue
108                 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
109         choices += '55-DOOR '
110         choices += 'OR A SNACK. '
111         choices += '99 TO READ AGAIN. '
112         choices += 'CHOICE?   '
113         msg.append((choices, False, None))
114         mk.set_messages(msg)
115
116 def get_pin(uid):
117         try:
118                 info = pwd.getpwuid(uid)
119         except KeyError:
120                 logging.info('getting pin for uid %d: user not in password file'%uid)
121                 return None
122         if info.pw_dir == None: return False
123         pinfile = os.path.join(info.pw_dir, '.pin')
124         try:
125                 s = os.stat(pinfile)
126         except OSError:
127                 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
128                 return None
129         if s.st_mode & 077:
130                 logging.info('getting pin for uid %d: .pin has wrong permissions'%uid)
131                 return None
132         try:
133                 f = file(pinfile)
134         except IOError:
135                 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
136                 return None
137         pinstr = f.readline()
138         f.close()
139         if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
140                 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
141                 return None
142         return int(pinstr)
143
144 def has_good_pin(uid):
145         return get_pin(uid) != None
146
147 def verify_user_pin(uid, pin):
148         if get_pin(uid) == pin:
149                 info = pwd.getpwuid(uid)
150                 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
151                 return info.pw_name
152         else:
153                 logging.info('refused pin for uid %d'%(uid))
154                 return None
155
156
157 def cookie(v):
158         seed(time())
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)
165         reveal = 1
166         while left:
167                 s = ''
168                 for i in range(0, len(msg)):
169                         if i in left:
170                                 if reveal == 0:
171                                         left.remove(i)
172                                         s += msg[i]
173                                 else:
174                                         s += chr(int(random()*26)+ord('A'))
175                                 reveal += 1
176                                 reveal %= 17
177                         else:
178                                 s += msg[i]
179                 v.display(s)
180
181 def center(str):
182         LEN = 10
183         return ' '*((LEN-len(str))/2)+str
184
185
186
187 idlers = []
188 idler = None
189
190 def setup_idlers(v):
191         global idlers, idler
192         idlers = [
193                  GrayIdler(v),
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                 ClockIdler(v),
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                 ClockIdler(v),
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"),
209                 FortuneIdler(v),
210                 ClockIdler(v),
211                 StringIdler(v),
212                 TrainIdler(v),
213                 ]
214         disabled = [
215                 ]
216         idler = choose_idler()
217
218 def choose_idler():
219         global idlers, idler
220         iiindex = 0
221
222         if idler:
223                 iiindex = idlers.index(idler)
224
225         iilen = len(idlers)
226
227         move = int(random()*len(idlers)) + 1
228
229         while move >= 0:
230                 idler = idlers[( (iiindex + 1) % iilen)]
231                 move = move - idler.affinity()
232
233         idler.reset()
234
235 def idle_step():
236         global idler
237         if idler.finished():
238                 choose_idler()
239         sleep(IDLE_SPEED)
240         idler.next()
241
242 class VendState:
243         def __init__(self,v):
244                 self.state_table = {}
245                 self.state = STATE_IDLE
246                 self.counter = 0
247
248                 self.mk = MessageKeeper(v)
249                 self.cur_user = ''
250                 self.cur_pin = ''
251                 self.username = ''
252                 self.cur_selection = ''
253                 self.time_to_autologout = None
254
255                 self.time_to_idle = None
256
257                 self.last_timeout_refresh = None
258
259         def change_state(self,newstate,newcounter=None):
260                 if self.state != newstate:
261                         #print "Changing state from: ", 
262                         #print self.state,
263                         #print " to ", 
264                         #print newstate 
265                         self.state = newstate
266
267                 if newcounter is not None and self.counter != newcounter:
268                         #print "Changing counter from: ", 
269                         #print self.counter,
270                         #print " to ", 
271                         #print newcounter 
272                         self.counter = newcounter
273
274
275
276 def handle_tick_event(event, params, v, vstatus):
277         # don't care right now.
278         pass
279
280 def handle_switch_event(event, params, v, vstatus):
281         # don't care right now.
282         pass
283
284
285 def do_nothing(state, event, params, v, vstatus):
286         print "doing nothing (s,e,p)", state, " ", event, " ", params
287         pass
288
289 def handle_getting_uid_idle(state, event, params, v, vstatus):
290         # don't care right now.
291         pass
292
293 def handle_getting_pin_idle(state, event, params, v, vstatus):
294         # don't care right now.
295         pass
296
297 def handle_get_selection_idle(state, event, params, v, vstatus):
298         # don't care right now.
299         ###
300         ### State logging out ..
301         if vstatus.time_to_autologout != None:
302                 time_left = vstatus.time_to_autologout - time()
303                 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
304                         vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
305                         vstatus.last_timeout_refresh = int(time_left)
306                         vstatus.cur_selection = ''
307
308         if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
309                 vstatus.time_to_autologout = None
310                 vstatus.cur_user = ''
311                 vstatus.cur_pin = ''
312                 vstatus.cur_selection = ''
313                         
314                 idle_in(vstatus,2)
315                 vstatus.change_state(STATE_IDLE)
316
317                 vstatus.mk.set_message(GREETING)
318
319         ### State fully logged out ... reset variables
320         if vstatus.time_to_autologout and not vstatus.mk.done(): 
321                 vstatus.time_to_autologout = None
322         if vstatus.cur_user == '' and vstatus.time_to_autologout: 
323                 vstatus.time_to_autologout = None
324         
325         ### State logged in
326         if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
327                 # start autologout
328                 vstatus.time_to_autologout = time() + 15
329                 vstatus.last_timeout_refresh = None
330
331         ### State logged out ... after normal logout??
332         # perhaps when logged in?
333         if vstatus.time_to_idle is not None and vstatus.cur_user != '': 
334                 vstatus.time_to_idle = None
335
336
337         ## FIXME - this may need to be elsewhere.....
338         # need to check
339         vstatus.mk.update_display()
340
341
342
343 def handle_get_selection_key(state, event, params, v, vstatus):
344         key = params
345         if len(vstatus.cur_selection) == 0:
346                 if key == 11:
347                         vstatus.cur_pin = ''
348                         vstatus.cur_user = ''
349                         vstatus.cur_selection = ''
350                         
351                         idle_in(vstatus,2)
352                         vstatus.change_state(STATE_IDLE)
353
354                         vstatus.mk.set_messages(
355                                 [(center('BYE!'), False, 1.5),
356                                  (GREETING, False, None)])
357                         return
358                 vstatus.cur_selection += chr(key + ord('0'))
359                 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
360                 vstatus.time_to_autologout = None
361         elif len(vstatus.cur_selection) == 1:
362                 if key == 11:
363                         vstatus.cur_selection = ''
364                         vstatus.time_to_autologout = None
365                         scroll_options(vstatus.username, vstatus.mk)
366                         return
367                 else:
368                         vstatus.cur_selection += chr(key + ord('0'))
369                         make_selection(v,vstatus)
370                         vstatus.cur_selection = ''
371                         vstatus.time_to_autologout = time() + 8
372                         vstatus.last_timeout_refresh = None
373
374 def make_selection(v, vstatus):
375         # should use sudo here
376         if vstatus.cur_selection == '55':
377                 vstatus.mk.set_message('OPENSESAME')
378                 logging.info('dispensing a door for %s'%vstatus.username)
379                 if geteuid() == 0:
380                         ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
381                 else:
382                         ret = os.system('dispense door')
383                 if ret == 0:
384                         logging.info('door opened')
385                         vstatus.mk.set_message(center('DOOR OPEN'))
386                 else:
387                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
388                         vstatus.mk.set_message(center('BAD DOOR'))
389                 sleep(1)
390         elif vstatus.cur_selection == '91':
391                 cookie(v)
392         elif vstatus.cur_selection == '99':
393                 scroll_options(vstatus.username, vstatus.mk)
394                 vstatus.cur_selection = ''
395                 return
396         elif vstatus.cur_selection[1] == '8':
397                 v.display('GOT COKE?')
398                 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
399                         v.display('SEEMS NOT')
400                 else:
401                         v.display('GOT COKE!')
402         else:
403                 v.display(vstatus.cur_selection+' - $1.00')
404                 if ((os.system('su - "%s" -c "dispense snack"'%(vstatus.username)) >> 8) == 0):
405                         v.vend(vstatus.cur_selection)
406                         v.display('THANK YOU')
407                 else:
408                         v.display('NO MONEY?')
409         sleep(1)
410
411
412 def handle_getting_pin_key(state, event, params, v, vstatus):
413         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
414         key = params
415         if len(vstatus.cur_pin) < PIN_LENGTH:
416                 if key == 11:
417                         if vstatus.cur_pin == '':
418                                 vstatus.cur_user = ''
419                                 vstatus.mk.set_message(GREETING)
420                         
421                                 idle_in(vstatus,5)
422                                 vstatus.change_state(STATE_IDLE)
423
424                                 return
425                         vstatus.cur_pin = ''
426                         vstatus.mk.set_message('PIN: ')
427                         return
428                 vstatus.cur_pin += chr(key + ord('0'))
429                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
430                 if len(vstatus.cur_pin) == PIN_LENGTH:
431                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
432                         if vstatus.username:
433                                 v.beep(0, False)
434                                 vstatus.cur_selection = ''
435                                 vstatus.change_state(STATE_GET_SELECTION)
436                                 scroll_options(vstatus.username, vstatus.mk, True)
437                                 return
438                         else:
439                                 v.beep(40, False)
440                                 vstatus.mk.set_messages(
441                                         [(center('BAD PIN'), False, 1.0),
442                                          (center('SORRY'), False, 0.5),
443                                          (GREETING, False, None)])
444                                 vstatus.cur_user = ''
445                                 vstatus.cur_pin = ''
446                         
447                                 idle_in(vstatus,5)
448                                 vstatus.change_state(STATE_IDLE)
449
450                                 return
451
452
453 def handle_getting_uid_key(state, event, params, v, vstatus):
454         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
455         key = params
456         # complicated key handling here:
457         if len(vstatus.cur_user) < 5:
458                 if key == 11:
459                         vstatus.cur_user = ''
460                         vstatus.mk.set_message(GREETING)
461
462                         idle_in(vstatus,5)
463                         vstatus.change_state(STATE_IDLE)
464                         return
465
466                 vstatus.cur_user += chr(key + ord('0'))
467                 vstatus.mk.set_message('UID: '+vstatus.cur_user)
468
469         if len(vstatus.cur_user) == 5:
470                 uid = int(vstatus.cur_user)
471                 if not has_good_pin(uid):
472                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
473                         vstatus.mk.set_messages(
474                                 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
475                                  (GREETING, False, None)])
476                         vstatus.cur_user = ''
477                         vstatus.cur_pin = ''
478                         
479                         idle_in(vstatus,5)
480                         vstatus.change_state(STATE_IDLE)
481
482                         return
483
484
485                 vstatus.cur_pin = ''
486                 vstatus.mk.set_message('PIN: ')
487                 logging.info('need pin for user %s'%vstatus.cur_user)
488                 vstatus.change_state(STATE_GETTING_PIN)
489                 return
490
491
492 def handle_idle_key(state, event, params, v, vstatus):
493         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
494
495         key = params
496
497         if key == 11:
498                 vstatus.cur_user = ''
499                 vstatus.mk.set_message(GREETING)
500                 idle_in(vstatus,5)
501                 choose_idler()
502                 return
503         
504         vstatus.change_state(STATE_GETTING_UID)
505         run_handler(event, key, v, vstatus)
506
507
508 def handle_idle_tick(state, event, params, v, vstatus):
509         ### State logged out ... initiate idler in 5  (first start?)
510         if vstatus.time_to_idle == None and vstatus.cur_user == '':
511                 vstatus.time_to_idle = time() + 5
512                 choose_idler()
513
514         ### State idling
515
516         if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle: 
517                 idle_step()
518
519         if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle + 30:
520                 vstatus.time_to_idle = time()
521                 choose_idler()
522         
523         ###
524
525         vstatus.mk.update_display()
526
527         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
528         run_handler(event, params, v, vstatus)
529         sleep(0.05)
530
531 def beep_on(when, before=0):
532         start = int(when - before)
533         end = int(when)
534         now = int(time())
535
536         if now >= start and now <= end:
537                 return 1
538         return 0
539
540 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
541         ### check for interesting times
542         now = localtime()
543
544         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
545         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
546         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
547         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
548
549         hourfromnow = localtime(time() + 3600)
550         
551         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
552         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
553                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
554
555         ## check for X seconds to the hour
556         ## if case, update counter to 2
557         if beep_on(onthehour,15) \
558                 or beep_on(halfhour,0) \
559                 or beep_on(quarterhour,0) \
560                 or beep_on(threequarterhour,0) \
561                 or beep_on(fivetothehour,0):
562                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
563                 run_handler(event, params, v, vstatus)
564         else:
565                 vstatus.change_state(STATE_IDLE)
566
567 def handle_grandfather_tick(state, event, params, v, vstatus):
568         go_idle = 1
569
570         msg = []
571         ### we live in interesting times
572         now = localtime()
573
574         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
575         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
576         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
577         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
578
579         hourfromnow = localtime(time() + 3600)
580         
581 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
582         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
583                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
584
585
586         #print "when it fashionable to wear a onion on your hip"
587
588         if beep_on(onthehour,15):
589                 go_idle = 0
590                 next_hour=((hourfromnow[3] + 11) % 12) + 1
591                 if onthehour - time() < next_hour and onthehour - time() > 0:
592                         v.beep(0, False)
593
594                         t = int(time())
595                         if (t % 2) == 0:
596                                 msg.append(("DING!", False, None))
597                         else:
598                                 msg.append(("     DING!", False, None))
599                 elif int(onthehour - time()) == 0:
600                         v.beep(255, False)
601                         msg.append(("   BONG!", False, None))
602                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
603         elif beep_on(halfhour,0):
604                 go_idle = 0
605                 v.beep(0, False)
606                 msg.append((" HALFHOUR ", False, 50))
607         elif beep_on(quarterhour,0):
608                 go_idle = 0
609                 v.beep(0, False)
610                 msg.append((" QTR HOUR ", False, 50))
611         elif beep_on(threequarterhour,0):
612                 go_idle = 0
613                 v.beep(0, False)
614                 msg.append((" 3 QTR HR ", False, 50))
615         elif beep_on(fivetothehour,0):
616                 go_idle = 0
617                 v.beep(0, False)
618                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
619         else:
620                 go_idle = 1
621         
622         ## check for X seconds to the hour
623
624         if len(msg):
625                 vstatus.mk.set_messages(msg)
626                 sleep(1)
627
628         vstatus.mk.update_display()
629         ## if no longer case, return to idle
630
631         ## change idler to be clock
632         if go_idle and vstatus.mk.done():
633                 vstatus.change_state(STATE_IDLE,1)
634
635 def handle_door_idle(state, event, params, v, vstatus):
636         # don't care right now.
637         pass
638
639 def handle_door_event(state, event, params, v, vstatus):
640         vstatus.time_to_idle = None
641
642         if params == 1:  #door open
643                 vstatus.change_state(STATE_DOOR_OPENING)
644                 logging.warning("Entering open door mode")
645                 v.display("-FEED  ME-")
646                 #door_open_mode(v);
647                 vstatus.cur_user = ''
648                 vstatus.cur_pin = ''
649         elif params == 0:  #door closed
650                 vstatus.change_state(STATE_DOOR_CLOSING)
651                 idle_in(vstatus, 5)
652
653                 logging.warning('Leaving open door mode')
654                 v.display("-YUM YUM!-")
655
656 def idle_in(vstatus,seconds):
657         vstatus.time_to_idle = time() + seconds
658
659 def return_to_idle(state,event,params,v,vstatus):
660         if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle: 
661                 vstatus.mk.set_message(GREETING)
662                 vstatus.change_state(STATE_IDLE)
663                 return
664         if not vstatus.time_to_idle:
665                 vstatus.mk.set_message(GREETING)
666                 vstatus.change_state(STATE_IDLE)
667                 return
668
669 def create_state_table(vstatus):
670         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
671         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
672         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
673
674         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
675         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
676         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
677
678         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
679         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
680         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
681
682         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
683         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
684         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
685
686         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
687         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
688         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
689
690         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
691         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
692         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
693
694         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
695         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
696         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
697         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
698         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
699         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
700
701 def get_state_table_handler(vstatus, state, event, counter):
702         return vstatus.state_table[(state,event,counter)]
703
704 def run_forever(rfh, wfh, options, cf):
705         v = VendingMachine(rfh, wfh)
706         vstatus = VendState(v)
707         create_state_table(vstatus)
708
709         logging.debug('PING is ' + str(v.ping()))
710
711         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
712
713         vstatus.mk.set_message(GREETING)
714         setup_idlers(v)
715         choose_idler()
716         vstatus.mk.set_message("Booted")
717
718
719         # This main loop was hideous and the work of the devil.
720         # This has now been fixed (mostly) - mtearle
721         #
722         #
723         # notes for later surgery
724         #   (event, counter, ' ')
725         #        V
726         #   d[      ] = (method)
727         #
728         # ( return state - not currently implemented )
729
730         vstatus.change_state(STATE_IDLE,1)
731
732         while True:
733                 if USE_DB:
734                         try:
735                                 db.handle_events()
736                         except DispenseDatabaseException, e:
737                                 logging.error('Database error: '+str(e))
738
739
740                 e = v.next_event(0)
741                 (event, params) = e
742
743                 run_handler(event, params, v, vstatus)
744
745 #               logging.debug('Got event: ' + repr(e))
746
747
748 def run_handler(event, params, v, vstatus):
749         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
750         if handler:
751                 handler(vstatus.state, event, params, v, vstatus)
752
753 def connect_to_vend(options, cf):
754
755         if options.use_lat:
756                 logging.info('Connecting to vending machine using LAT')
757                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
758                 rfh, wfh = latclient.get_fh()
759         elif options.use_serial:
760                 # Open vending machine via serial.
761                 logging.info('Connecting to vending machine using serial')
762                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
763                 rfh,wfh = serialclient.get_fh()
764         else:
765                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
766                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
767                 import socket
768                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
769                 sock.connect((options.host, options.port))
770                 rfh = sock.makefile('r')
771                 wfh = sock.makefile('w')
772                 
773         return rfh, wfh
774
775 def parse_args():
776         from optparse import OptionParser
777
778         op = OptionParser(usage="%prog [OPTION]...")
779         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')
780         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
781         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
782         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
783         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
784         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
785         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
786         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
787         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
788         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
789         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
790         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
791         options, args = op.parse_args()
792
793         if len(args) != 0:
794                 op.error('extra command line arguments: ' + ' '.join(args))
795
796         return options
797
798 config_options = {
799         'DBServer': ('Database', 'Server'),
800         'DBName': ('Database', 'Name'),
801         'DBUser': ('VendingMachine', 'DBUser'),
802         'DBPassword': ('VendingMachine', 'DBPassword'),
803         
804         'ServiceName': ('VendingMachine', 'ServiceName'),
805         'ServicePassword': ('VendingMachine', 'Password'),
806         
807         'ServerName': ('DecServer', 'Name'),
808         'ConnectPassword': ('DecServer', 'ConnectPassword'),
809         'PrivPassword': ('DecServer', 'PrivPassword'),
810         }
811
812 class VendConfigFile:
813         def __init__(self, config_file, options):
814                 try:
815                         cp = ConfigParser.ConfigParser()
816                         cp.read(config_file)
817
818                         for option in options:
819                                 section, name = options[option]
820                                 value = cp.get(section, name)
821                                 self.__dict__[option] = value
822                 
823                 except ConfigParser.Error, e:
824                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
825
826 def create_pid_file(name):
827         try:
828                 pid_file = file(name, 'w')
829                 pid_file.write('%d\n'%os.getpid())
830                 pid_file.close()
831         except IOError, e:
832                 logging.warning('unable to write to pid file '+name+': '+str(e))
833
834 def set_stuff_up():
835         def do_nothing(signum, stack):
836                 signal.signal(signum, do_nothing)
837         def stop_server(signum, stack): raise KeyboardInterrupt
838         signal.signal(signal.SIGHUP, do_nothing)
839         signal.signal(signal.SIGTERM, stop_server)
840         signal.signal(signal.SIGINT, stop_server)
841
842         options = parse_args()
843         config_opts = VendConfigFile(options.config_file, config_options)
844         if options.daemon: become_daemon()
845         set_up_logging(options)
846         if options.pid_file != '': create_pid_file(options.pid_file)
847
848         return options, config_opts
849
850 def clean_up_nicely(options, config_opts):
851         if options.pid_file != '':
852                 try:
853                         os.unlink(options.pid_file)
854                         logging.debug('Removed pid file '+options.pid_file)
855                 except OSError: pass  # if we can't delete it, meh
856
857 def set_up_logging(options):
858         logger = logging.getLogger()
859         
860         if not options.daemon:
861                 stderr_logger = logging.StreamHandler(sys.stderr)
862                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
863                 logger.addHandler(stderr_logger)
864         
865         if options.log_file != '':
866                 try:
867                         file_logger = logging.FileHandler(options.log_file)
868                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
869                         logger.addHandler(file_logger)
870                 except IOError, e:
871                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
872
873         if options.syslog != None:
874                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
875                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
876                 logger.addHandler(sys_logger)
877
878         if options.quiet:
879                 logger.setLevel(logging.WARNING)
880         elif options.verbose:
881                 logger.setLevel(logging.DEBUG)
882         else:
883                 logger.setLevel(logging.INFO)
884
885 def become_daemon():
886         dev_null = file('/dev/null')
887         fd = dev_null.fileno()
888         os.dup2(fd, 0)
889         os.dup2(fd, 1)
890         os.dup2(fd, 2)
891         try:
892                 if os.fork() != 0:
893                         sys.exit(0)
894                 os.setsid()
895         except OSError, e:
896                 raise SystemExit('failed to fork: '+str(e))
897
898 def do_vend_server(options, config_opts):
899         while True:
900                 try:
901                         rfh, wfh = connect_to_vend(options, config_opts)
902                 except (SerialClientException, socket.error), e:
903                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
904                         del exc_traceback
905                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
906                         logging.info("Trying again in 5 seconds.")
907                         sleep(5)
908                         continue
909                 
910                 try:
911                         run_forever(rfh, wfh, options, config_opts)
912                 except VendingException:
913                         logging.error("Connection died, trying again...")
914                         logging.info("Trying again in 5 seconds.")
915                         sleep(5)
916
917 if __name__ == '__main__':
918         options, config_opts = set_stuff_up()
919         while True:
920                 try:
921                         logging.warning('Starting Vend Server')
922                         do_vend_server(options, config_opts)
923                         logging.error('Vend Server finished unexpectedly, restarting')
924                 except KeyboardInterrupt:
925                         logging.info("Killed by signal, cleaning up")
926                         clean_up_nicely(options, config_opts)
927                         logging.warning("Vend Server stopped")
928                         break
929                 except SystemExit:
930                         break
931                 except:
932                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
933                         tb = format_tb(exc_traceback, 20)
934                         del exc_traceback
935                         
936                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
937                         logging.critical("Message: " + str(exc_value))
938                         logging.critical("Traceback:")
939                         for event in tb:
940                                 for line in event.split('\n'):
941                                         logging.critical('    '+line)
942                         logging.critical("This message should be considered a bug in the Vend Server.")
943                         logging.critical("Please report this to someone who can fix it.")
944                         sleep(10)
945                         logging.warning("Trying again anyway (might not help, but hey...)")
946

UCC git Repository :: git.ucc.asn.au