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

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