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

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