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

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