dd2cd7d93088255b481905877a8075e54f826122
[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 GreetingIdler,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 PIN_LENGTH = 4
40
41 DOOR = 1
42 SWITCH = 2
43 KEY = 3
44 TICK = 4
45
46
47 STATE_IDLE = 1
48 STATE_DOOR_OPENING = 2
49 STATE_DOOR_CLOSING = 3
50 STATE_GETTING_UID = 4
51 STATE_GETTING_PIN = 5
52 STATE_GET_SELECTION = 6
53 STATE_GRANDFATHER_CLOCK = 7
54
55 TEXT_SPEED = 0.8
56 IDLE_SPEED = 0.05
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. Fixing.'%uid)
130                 os.chmod(pinfile, 0600)
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
216 def reset_idler(v, vstatus, t = None):
217         global idlers, idler
218         idler = GreetingIdler(v, t)
219         vstatus.time_of_next_idlestep = time()+idler.next()
220         vstatus.time_of_next_idler = None
221         vstatus.change_state(STATE_IDLE, 1)
222
223 def choose_idler():
224         global idlers, idler
225         iiindex = 0
226         average_affinity = 10 # guessing here...
227
228         if idler and idler.__class__ != GreetingIdler:
229                 iiindex = idlers.index(idler)
230
231         iilen = len(idlers)
232
233         move = int(random()*len(idlers)*average_affinity) + 1
234
235         while move >= 0:
236                 iiindex += 1
237                 iiindex %= iilen
238                 idler = idlers[iiindex]
239                 move -= idler.affinity()
240
241         idler.reset()
242
243 def idle_step(vstatus):
244         global idler
245         if idler.finished():
246                 choose_idler()
247                 vstatus.time_of_next_idler = time() + 30
248         nextidle = idler.next()
249         if nextidle is None:
250                 nextidle = IDLE_SPEED
251         vstatus.time_of_next_idlestep = time()+nextidle
252
253 class VendState:
254         def __init__(self,v):
255                 self.state_table = {}
256                 self.state = STATE_IDLE
257                 self.counter = 0
258
259                 self.mk = MessageKeeper(v)
260                 self.cur_user = ''
261                 self.cur_pin = ''
262                 self.username = ''
263                 self.cur_selection = ''
264                 self.time_to_autologout = None
265
266                 self.last_timeout_refresh = None
267
268         def change_state(self,newstate,newcounter=None):
269                 if self.state != newstate:
270                         #print "Changing state from: ", 
271                         #print self.state,
272                         #print " to ", 
273                         #print newstate 
274                         self.state = newstate
275
276                 if newcounter is not None and self.counter != newcounter:
277                         #print "Changing counter from: ", 
278                         #print self.counter,
279                         #print " to ", 
280                         #print newcounter 
281                         self.counter = newcounter
282
283
284
285 def handle_tick_event(event, params, v, vstatus):
286         # don't care right now.
287         pass
288
289 def handle_switch_event(event, params, v, vstatus):
290         # don't care right now.
291         pass
292
293
294 def do_nothing(state, event, params, v, vstatus):
295         print "doing nothing (s,e,p)", state, " ", event, " ", params
296         pass
297
298 def handle_getting_uid_idle(state, event, params, v, vstatus):
299         # don't care right now.
300         pass
301
302 def handle_getting_pin_idle(state, event, params, v, vstatus):
303         # don't care right now.
304         pass
305
306 def handle_get_selection_idle(state, event, params, v, vstatus):
307         # don't care right now.
308         ###
309         ### State logging out ..
310         if vstatus.time_to_autologout != None:
311                 time_left = vstatus.time_to_autologout - time()
312                 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
313                         vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
314                         vstatus.last_timeout_refresh = int(time_left)
315                         vstatus.cur_selection = ''
316
317         if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
318                 vstatus.time_to_autologout = None
319                 vstatus.cur_user = ''
320                 vstatus.cur_pin = ''
321                 vstatus.cur_selection = ''
322                         
323                 reset_idler(v, vstatus)
324
325         ### State fully logged out ... reset variables
326         if vstatus.time_to_autologout and not vstatus.mk.done(): 
327                 vstatus.time_to_autologout = None
328         if vstatus.cur_user == '' and vstatus.time_to_autologout: 
329                 vstatus.time_to_autologout = None
330         
331         ### State logged in
332         if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
333                 # start autologout
334                 vstatus.time_to_autologout = time() + 15
335                 vstatus.last_timeout_refresh = None
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                         reset_idler(v, vstatus)
352
353                         vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
354                         return
355                 vstatus.cur_selection += chr(key + ord('0'))
356                 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
357                 vstatus.time_to_autologout = None
358         elif len(vstatus.cur_selection) == 1:
359                 if key == 11:
360                         vstatus.cur_selection = ''
361                         vstatus.time_to_autologout = None
362                         scroll_options(vstatus.username, vstatus.mk)
363                         return
364                 else:
365                         vstatus.cur_selection += chr(key + ord('0'))
366                         make_selection(v,vstatus)
367                         vstatus.cur_selection = ''
368                         vstatus.time_to_autologout = time() + 8
369                         vstatus.last_timeout_refresh = None
370
371 def make_selection(v, vstatus):
372         # should use sudo here
373         if vstatus.cur_selection == '55':
374                 vstatus.mk.set_message('OPENSESAME')
375                 logging.info('dispensing a door for %s'%vstatus.username)
376                 if geteuid() == 0:
377                         ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
378                 else:
379                         ret = os.system('dispense door')
380                 if ret == 0:
381                         logging.info('door opened')
382                         vstatus.mk.set_message(center('DOOR OPEN'))
383                 else:
384                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
385                         vstatus.mk.set_message(center('BAD DOOR'))
386                 sleep(1)
387         elif vstatus.cur_selection == '91':
388                 cookie(v)
389         elif vstatus.cur_selection == '99':
390                 scroll_options(vstatus.username, vstatus.mk)
391                 vstatus.cur_selection = ''
392                 return
393         elif vstatus.cur_selection[1] == '8':
394                 v.display('GOT COKE?')
395                 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
396                         v.display('SEEMS NOT')
397                 else:
398                         v.display('GOT COKE!')
399         else:
400                 v.display(vstatus.cur_selection+' - $1.00')
401                 if ((os.system('su - "%s" -c "dispense snack"'%(vstatus.username)) >> 8) == 0):
402                         v.vend(vstatus.cur_selection)
403                         v.display('THANK YOU')
404                 else:
405                         v.display('NO MONEY?')
406         sleep(1)
407
408
409 def handle_getting_pin_key(state, event, params, v, vstatus):
410         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
411         key = params
412         if len(vstatus.cur_pin) < PIN_LENGTH:
413                 if key == 11:
414                         if vstatus.cur_pin == '':
415                                 vstatus.cur_user = ''
416                                 reset_idler(v, vstatus)
417
418                                 return
419                         vstatus.cur_pin = ''
420                         vstatus.mk.set_message('PIN: ')
421                         return
422                 vstatus.cur_pin += chr(key + ord('0'))
423                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
424                 if len(vstatus.cur_pin) == PIN_LENGTH:
425                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
426                         if vstatus.username:
427                                 v.beep(0, False)
428                                 vstatus.cur_selection = ''
429                                 vstatus.change_state(STATE_GET_SELECTION)
430                                 scroll_options(vstatus.username, vstatus.mk, True)
431                                 return
432                         else:
433                                 v.beep(40, False)
434                                 vstatus.mk.set_messages(
435                                         [(center('BAD PIN'), False, 1.0),
436                                          (center('SORRY'), False, 0.5)])
437                                 vstatus.cur_user = ''
438                                 vstatus.cur_pin = ''
439                         
440                                 reset_idler(v, vstatus, 2)
441
442                                 return
443
444
445 def handle_getting_uid_key(state, event, params, v, vstatus):
446         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
447         key = params
448         # complicated key handling here:
449         if len(vstatus.cur_user) < 5:
450                 if key == 11:
451                         vstatus.cur_user = ''
452
453                         reset_idler(v, vstatus)
454                         return
455
456                 vstatus.cur_user += chr(key + ord('0'))
457                 vstatus.mk.set_message('UID: '+vstatus.cur_user)
458
459         if len(vstatus.cur_user) == 5:
460                 uid = int(vstatus.cur_user)
461                 if not has_good_pin(uid):
462                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
463                         vstatus.mk.set_messages(
464                                 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3)])
465                         vstatus.cur_user = ''
466                         vstatus.cur_pin = ''
467                         
468                         reset_idler(v, vstatus, 5)
469
470                         return
471
472
473                 vstatus.cur_pin = ''
474                 vstatus.mk.set_message('PIN: ')
475                 logging.info('need pin for user %s'%vstatus.cur_user)
476                 vstatus.change_state(STATE_GETTING_PIN)
477                 return
478
479
480 def handle_idle_key(state, event, params, v, vstatus):
481         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
482
483         key = params
484
485         if key == 11:
486                 vstatus.cur_user = ''
487                 reset_idler(v, vstatus)
488                 return
489         
490         vstatus.change_state(STATE_GETTING_UID)
491         run_handler(event, key, v, vstatus)
492
493
494 def handle_idle_tick(state, event, params, v, vstatus):
495         ### State idling
496         idle_step(vstatus)
497
498         if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
499                 vstatus.time_of_next_idler = time() + 30
500                 choose_idler()
501         
502         ###
503
504         vstatus.mk.update_display()
505
506         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
507         run_handler(event, params, v, vstatus)
508         sleep(0.05)
509
510 def beep_on(when, before=0):
511         start = int(when - before)
512         end = int(when)
513         now = int(time())
514
515         if now >= start and now <= end:
516                 return 1
517         return 0
518
519 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
520         ### check for interesting times
521         now = localtime()
522
523         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
524         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
525         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
526         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
527
528         hourfromnow = localtime(time() + 3600)
529         
530         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
531         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
532                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
533
534         ## check for X seconds to the hour
535         ## if case, update counter to 2
536         if beep_on(onthehour,15) \
537                 or beep_on(halfhour,0) \
538                 or beep_on(quarterhour,0) \
539                 or beep_on(threequarterhour,0) \
540                 or beep_on(fivetothehour,0):
541                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
542                 run_handler(event, params, v, vstatus)
543         else:
544                 vstatus.change_state(STATE_IDLE)
545
546 def handle_grandfather_tick(state, event, params, v, vstatus):
547         go_idle = 1
548
549         msg = []
550         ### we live in interesting times
551         now = localtime()
552
553         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
554         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
555         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
556         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
557
558         hourfromnow = localtime(time() + 3600)
559         
560 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
561         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
562                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
563
564
565         #print "when it fashionable to wear a onion on your hip"
566
567         if beep_on(onthehour,15):
568                 go_idle = 0
569                 next_hour=((hourfromnow[3] + 11) % 12) + 1
570                 if onthehour - time() < next_hour and onthehour - time() > 0:
571                         v.beep(0, False)
572
573                         t = int(time())
574                         if (t % 2) == 0:
575                                 msg.append(("DING!", False, None))
576                         else:
577                                 msg.append(("     DING!", False, None))
578                 elif int(onthehour - time()) == 0:
579                         v.beep(255, False)
580                         msg.append(("   BONG!", False, None))
581                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
582         elif beep_on(halfhour,0):
583                 go_idle = 0
584                 v.beep(0, False)
585                 msg.append((" HALFHOUR ", False, 50))
586         elif beep_on(quarterhour,0):
587                 go_idle = 0
588                 v.beep(0, False)
589                 msg.append((" QTR HOUR ", False, 50))
590         elif beep_on(threequarterhour,0):
591                 go_idle = 0
592                 v.beep(0, False)
593                 msg.append((" 3 QTR HR ", False, 50))
594         elif beep_on(fivetothehour,0):
595                 go_idle = 0
596                 v.beep(0, False)
597                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
598         else:
599                 go_idle = 1
600         
601         ## check for X seconds to the hour
602
603         if len(msg):
604                 vstatus.mk.set_messages(msg)
605                 sleep(1)
606
607         vstatus.mk.update_display()
608         ## if no longer case, return to idle
609
610         ## change idler to be clock
611         if go_idle and vstatus.mk.done():
612                 vstatus.change_state(STATE_IDLE,1)
613
614 def handle_door_idle(state, event, params, v, vstatus):
615         # don't care right now.
616         pass
617
618 def handle_door_event(state, event, params, v, vstatus):
619         if params == 1:  #door open
620                 vstatus.change_state(STATE_DOOR_OPENING)
621                 logging.warning("Entering open door mode")
622                 v.display("-FEED  ME-")
623                 #door_open_mode(v);
624                 vstatus.cur_user = ''
625                 vstatus.cur_pin = ''
626         elif params == 0:  #door closed
627                 vstatus.change_state(STATE_DOOR_CLOSING)
628                 reset_idler(v, vstatus, 3)
629
630                 logging.warning('Leaving open door mode')
631                 v.display("-YUM YUM!-")
632
633 def return_to_idle(state,event,params,v,vstatus):
634         reset_idler(v, vstatus)
635
636 def create_state_table(vstatus):
637         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
638         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
639         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
640
641         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
642         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
643         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
644
645         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
646         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
647         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
648
649         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
650         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
651         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
652
653         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
654         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
655         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
656
657         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
658         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
659         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
660
661         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
662         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
663         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
664         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
665         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
666         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
667
668 def get_state_table_handler(vstatus, state, event, counter):
669         return vstatus.state_table[(state,event,counter)]
670
671 def run_forever(rfh, wfh, options, cf):
672         v = VendingMachine(rfh, wfh)
673         vstatus = VendState(v)
674         create_state_table(vstatus)
675
676         logging.debug('PING is ' + str(v.ping()))
677
678         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
679
680         setup_idlers(v)
681         reset_idler(v, vstatus)
682
683         # This main loop was hideous and the work of the devil.
684         # This has now been fixed (mostly) - mtearle
685         #
686         #
687         # notes for later surgery
688         #   (event, counter, ' ')
689         #        V
690         #   d[      ] = (method)
691         #
692         # ( return state - not currently implemented )
693
694         while True:
695                 if USE_DB:
696                         try:
697                                 db.handle_events()
698                         except DispenseDatabaseException, e:
699                                 logging.error('Database error: '+str(e))
700
701
702                 e = v.next_event(vstatus.time_of_next_idlestep-time())
703                 (event, params) = e
704
705                 run_handler(event, params, v, vstatus)
706
707 #               logging.debug('Got event: ' + repr(e))
708
709
710 def run_handler(event, params, v, vstatus):
711         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
712         if handler:
713                 handler(vstatus.state, event, params, v, vstatus)
714
715 def connect_to_vend(options, cf):
716
717         if options.use_lat:
718                 logging.info('Connecting to vending machine using LAT')
719                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
720                 rfh, wfh = latclient.get_fh()
721         elif options.use_serial:
722                 # Open vending machine via serial.
723                 logging.info('Connecting to vending machine using serial')
724                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
725                 rfh,wfh = serialclient.get_fh()
726         else:
727                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
728                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
729                 import socket
730                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
731                 sock.connect((options.host, options.port))
732                 rfh = sock.makefile('r')
733                 wfh = sock.makefile('w')
734                 
735         return rfh, wfh
736
737 def parse_args():
738         from optparse import OptionParser
739
740         op = OptionParser(usage="%prog [OPTION]...")
741         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')
742         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
743         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
744         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
745         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
746         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
747         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
748         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
749         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
750         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
751         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
752         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
753         options, args = op.parse_args()
754
755         if len(args) != 0:
756                 op.error('extra command line arguments: ' + ' '.join(args))
757
758         return options
759
760 config_options = {
761         'DBServer': ('Database', 'Server'),
762         'DBName': ('Database', 'Name'),
763         'DBUser': ('VendingMachine', 'DBUser'),
764         'DBPassword': ('VendingMachine', 'DBPassword'),
765         
766         'ServiceName': ('VendingMachine', 'ServiceName'),
767         'ServicePassword': ('VendingMachine', 'Password'),
768         
769         'ServerName': ('DecServer', 'Name'),
770         'ConnectPassword': ('DecServer', 'ConnectPassword'),
771         'PrivPassword': ('DecServer', 'PrivPassword'),
772         }
773
774 class VendConfigFile:
775         def __init__(self, config_file, options):
776                 try:
777                         cp = ConfigParser.ConfigParser()
778                         cp.read(config_file)
779
780                         for option in options:
781                                 section, name = options[option]
782                                 value = cp.get(section, name)
783                                 self.__dict__[option] = value
784                 
785                 except ConfigParser.Error, e:
786                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
787
788 def create_pid_file(name):
789         try:
790                 pid_file = file(name, 'w')
791                 pid_file.write('%d\n'%os.getpid())
792                 pid_file.close()
793         except IOError, e:
794                 logging.warning('unable to write to pid file '+name+': '+str(e))
795
796 def set_stuff_up():
797         def do_nothing(signum, stack):
798                 signal.signal(signum, do_nothing)
799         def stop_server(signum, stack): raise KeyboardInterrupt
800         signal.signal(signal.SIGHUP, do_nothing)
801         signal.signal(signal.SIGTERM, stop_server)
802         signal.signal(signal.SIGINT, stop_server)
803
804         options = parse_args()
805         config_opts = VendConfigFile(options.config_file, config_options)
806         if options.daemon: become_daemon()
807         set_up_logging(options)
808         if options.pid_file != '': create_pid_file(options.pid_file)
809
810         return options, config_opts
811
812 def clean_up_nicely(options, config_opts):
813         if options.pid_file != '':
814                 try:
815                         os.unlink(options.pid_file)
816                         logging.debug('Removed pid file '+options.pid_file)
817                 except OSError: pass  # if we can't delete it, meh
818
819 def set_up_logging(options):
820         logger = logging.getLogger()
821         
822         if not options.daemon:
823                 stderr_logger = logging.StreamHandler(sys.stderr)
824                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
825                 logger.addHandler(stderr_logger)
826         
827         if options.log_file != '':
828                 try:
829                         file_logger = logging.FileHandler(options.log_file)
830                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
831                         logger.addHandler(file_logger)
832                 except IOError, e:
833                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
834
835         if options.syslog != None:
836                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
837                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
838                 logger.addHandler(sys_logger)
839
840         if options.quiet:
841                 logger.setLevel(logging.WARNING)
842         elif options.verbose:
843                 logger.setLevel(logging.DEBUG)
844         else:
845                 logger.setLevel(logging.INFO)
846
847 def become_daemon():
848         dev_null = file('/dev/null')
849         fd = dev_null.fileno()
850         os.dup2(fd, 0)
851         os.dup2(fd, 1)
852         os.dup2(fd, 2)
853         try:
854                 if os.fork() != 0:
855                         sys.exit(0)
856                 os.setsid()
857         except OSError, e:
858                 raise SystemExit('failed to fork: '+str(e))
859
860 def do_vend_server(options, config_opts):
861         while True:
862                 try:
863                         rfh, wfh = connect_to_vend(options, config_opts)
864                 except (SerialClientException, socket.error), e:
865                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
866                         del exc_traceback
867                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
868                         logging.info("Trying again in 5 seconds.")
869                         sleep(5)
870                         continue
871                 
872                 try:
873                         run_forever(rfh, wfh, options, config_opts)
874                 except VendingException:
875                         logging.error("Connection died, trying again...")
876                         logging.info("Trying again in 5 seconds.")
877                         sleep(5)
878
879 if __name__ == '__main__':
880         options, config_opts = set_stuff_up()
881         while True:
882                 try:
883                         logging.warning('Starting Vend Server')
884                         do_vend_server(options, config_opts)
885                         logging.error('Vend Server finished unexpectedly, restarting')
886                 except KeyboardInterrupt:
887                         logging.info("Killed by signal, cleaning up")
888                         clean_up_nicely(options, config_opts)
889                         logging.warning("Vend Server stopped")
890                         break
891                 except SystemExit:
892                         break
893                 except:
894                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
895                         tb = format_tb(exc_traceback, 20)
896                         del exc_traceback
897                         
898                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
899                         logging.critical("Message: " + str(exc_value))
900                         logging.critical("Traceback:")
901                         for event in tb:
902                                 for line in event.split('\n'):
903                                         logging.critical('    '+line)
904                         logging.critical("This message should be considered a bug in the Vend Server.")
905                         logging.critical("Please report this to someone who can fix it.")
906                         sleep(10)
907                         logging.warning("Trying again anyway (might not help, but hey...)")
908

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