chmod pin automatically. remove boot up message now it doesnt take so long to boot.
[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. Fixing.'%uid)
131                 os.chmod(pinfile, 0600)
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         setup_idlers(v)
714         choose_idler()
715         vstatus.mk.set_message(GREETING)
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