Rearranging file and adding comments
[uccvend-vendserver.git] / VendServer / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5 USE_MIFARE = 1
6
7 import ConfigParser
8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
11 if USE_DB: import pg
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
22 import socket
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
25 from OpenDispense import OpenDispense as Dispense
26
27 CREDITS="""
28 This vending machine software brought to you by:
29 Bernard Blackham
30 Mark Tearle
31 Nick Bannon
32 Cameron Patrick
33 and a collective of hungry alpacas.
34
35 The MIFARE card reader bought to you by:
36 David Adam
37
38 Bug Hunting and hardware maintenance by:
39 Mitchell Pomery
40
41 For a good time call +61 8 6488 3901
42
43
44
45 """
46
47 PIN_LENGTH = 4
48
49 DOOR = 1
50 SWITCH = 2
51 KEY = 3
52 TICK = 4
53 MIFARE = 5
54
55
56 (
57 STATE_IDLE,
58 STATE_DOOR_OPENING,
59 STATE_DOOR_CLOSING,
60 STATE_GETTING_UID,
61 STATE_GETTING_PIN,
62 STATE_GET_SELECTION,
63 STATE_GRANDFATHER_CLOCK,
64 ) = range(1,8)
65
66 TEXT_SPEED = 0.8
67 IDLE_SPEED = 0.05
68
69 _pin_uid = 0
70 _pin_uname = 'root'
71 _pin_pin = '----'
72
73 _last_card_id = -1
74
75 idlers = []
76 idler = None
77
78 config_options = {
79         'DBServer': ('Database', 'Server'),
80         'DBName': ('Database', 'Name'),
81         'DBUser': ('VendingMachine', 'DBUser'),
82         'DBPassword': ('VendingMachine', 'DBPassword'),
83         
84         'ServiceName': ('VendingMachine', 'ServiceName'),
85         'ServicePassword': ('VendingMachine', 'Password'),
86         
87         'ServerName': ('DecServer', 'Name'),
88         'ConnectPassword': ('DecServer', 'ConnectPassword'),
89         'PrivPassword': ('DecServer', 'PrivPassword'),
90         }
91
92 class VendConfigFile:
93         def __init__(self, config_file, options):
94                 try:
95                         cp = ConfigParser.ConfigParser()
96                         cp.read(config_file)
97
98                         for option in options:
99                                 section, name = options[option]
100                                 value = cp.get(section, name)
101                                 self.__dict__[option] = value
102                 
103                 except ConfigParser.Error, e:
104                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
105
106 class DispenseDatabaseException(Exception): pass
107
108 class DispenseDatabase:
109         def __init__(self, vending_machine, host, name, user, password):
110                 self.vending_machine = vending_machine
111                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
112                 self.db.query('LISTEN vend_requests')
113
114         def process_requests(self):
115                 logging.debug('database processing')
116                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
117                 try:
118                         outstanding = self.db.query(query).getresult()
119                 except (pg.error,), db_err:
120                         raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
121                 for (id, slot) in outstanding:
122                         (worked, code, string) = self.vending_machine.vend(slot)
123                         logging.debug (str((worked, code, string)))
124                         if worked:
125                                 query = 'SELECT vend_success(%s)'%id
126                                 self.db.query(query).getresult()
127                         else:
128                                 query = 'SELECT vend_failed(%s)'%id
129                                 self.db.query(query).getresult()
130
131         def handle_events(self):
132                 notifier = self.db.getnotify()
133                 while notifier is not None:
134                         self.process_requests()
135                         notify = self.db.getnotify()
136 """
137 This class manages the current state of the vending machine.
138 """
139 class VendState:
140         def __init__(self,v):
141                 self.state_table = {}
142                 self.state = STATE_IDLE
143                 self.counter = 0
144
145                 self.mk = MessageKeeper(v)
146                 self.cur_user = ''
147                 self.cur_pin = ''
148                 self.username = ''
149                 self.cur_selection = ''
150                 self.time_to_autologout = None
151
152                 self.last_timeout_refresh = None
153
154         def change_state(self,newstate,newcounter=None):
155                 if self.state != newstate:
156                         self.state = newstate
157
158                 if newcounter is not None and self.counter != newcounter:
159                         self.counter = newcounter
160
161 """
162 Show information to the user as to what can be dispensed.
163 """
164 def scroll_options(username, mk, welcome = False):
165         # If the user has just logged in, show them their balance
166         if welcome:
167                 # Balance checking
168                 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
169                 # this is fucking appalling
170                 balance = acct[acct.find("$")+1:acct.find("(")].strip()
171         
172                 msg = [(center('WELCOME'), False, TEXT_SPEED),
173                            (center(username), False, TEXT_SPEED),
174                            (center(balance), False, TEXT_SPEED),]
175         else:
176                 msg = []
177         choices = ' '*10+'CHOICES: '
178
179         # Show what is in the coke machine
180         cokes = []
181         for i in range(0, 7):
182                 args = ('dispense', 'iteminfo', 'coke:%i' % i)
183                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
184                 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
185                 cents = int(m.group(1))*100 + int(m.group(2))
186                 cokes.append('%i %i %s' % (i, cents, m.group(3)));
187
188         for c in cokes:
189                 c = c.strip()
190                 (slot_num, price, slot_name) = c.split(' ', 2)
191                 if slot_name == 'dead': continue
192                 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
193
194         # Show the final few options
195         choices += '55-DOOR '
196         choices += 'OR ANOTHER SNACK. '
197         choices += '99 TO READ AGAIN. '
198         choices += 'CHOICE?   '
199         msg.append((choices, False, None))
200         # Send it to the display
201         mk.set_messages(msg)
202
203
204 """
205 Verify the users pin
206 """
207 def _check_pin(uid, pin):
208         global _pin_uid
209         global _pin_uname
210         global _pin_pin
211         print "_check_pin('",uid,"',---)"
212         if uid != _pin_uid:
213                 try:
214                         info = pwd.getpwuid(uid)
215                 except KeyError:
216                         logging.info('getting pin for uid %d: user not in password file'%uid)
217                         return None
218                 if info.pw_dir == None: return False
219                 pinfile = os.path.join(info.pw_dir, '.pin')
220                 try:
221                         s = os.stat(pinfile)
222                 except OSError:
223                         logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
224                         return None
225                 if s.st_mode & 077:
226                         logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
227                         os.chmod(pinfile, 0600)
228                 try:
229                         f = file(pinfile)
230                 except IOError:
231                         logging.info('getting pin for uid %d: I cannot read pin file'%uid)
232                         return None
233                 pinstr = f.readline()
234                 f.close()
235                 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
236                         logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
237                         return None
238                 _pin_uid = uid
239                 _pin_pin = pinstr
240                 _pin_uname = info.pw_name
241         else:
242                 pinstr = _pin_pin
243         if pin == int(pinstr):
244                 logging.info("Pin correct for %d",uid)
245         else:
246                 logging.info("Pin incorrect for %d",uid)
247         return pin == int(pinstr)
248
249 """
250 Check if the users account has been disabled
251 """
252 def acct_is_disabled(name=None):
253         global _pin_uname
254         if name == None:
255                 name = _pin_uname
256         acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
257         # this is fucking appalling
258         flags = acct[acct.find("(")+1:acct.find(")")].strip()
259         if 'disabled' in flags:
260                 return True
261         if 'internal' in flags:
262                 return True
263         return False
264
265 """
266 Check that the user has a valid pin set
267 """
268 def has_good_pin(uid):
269         return _check_pin(uid, None) != None
270
271 """
272 Verify the users pin.
273 """
274 def verify_user_pin(uid, pin, skip_pin_check=False):
275         if skip_pin_check or _check_pin(uid, pin) == True:
276                 info = pwd.getpwuid(uid)
277                 if skip_pin_check:
278                         if acct_is_disabled(info.pw_name):
279                                 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
280                                 return '-disabled-'
281                         logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
282                 else:
283                         logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
284                 return info.pw_name
285         else:
286                 logging.info('refused pin for uid %d'%(uid))
287                 return None
288
289 """
290 In here just for fun.
291 """
292 def cookie(v):
293         seed(time())
294         messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
295         choice = int(random()*len(messages))
296         msg = messages[choice]
297         left = range(len(msg))
298         for i in range(len(msg)):
299                 if msg[i] == ' ': left.remove(i)
300         reveal = 1
301         while left:
302                 s = ''
303                 for i in range(0, len(msg)):
304                         if i in left:
305                                 if reveal == 0:
306                                         left.remove(i)
307                                         s += msg[i]
308                                 else:
309                                         s += chr(int(random()*26)+ord('A'))
310                                 reveal += 1
311                                 reveal %= 17
312                         else:
313                                 s += msg[i]
314                 v.display(s)
315
316 """
317 Format text so it will appear centered on the screen.
318 """
319 def center(str):
320         LEN = 10
321         return ' '*((LEN-len(str))/2)+str
322
323 """
324 Configure the things that will appear on screen whil the machine is idling.
325 """
326 def setup_idlers(v):
327         global idlers, idler
328         idlers = [
329                 #
330                 GrayIdler(v),
331                 GrayIdler(v,one="*",zero="-"),
332                 GrayIdler(v,one="/",zero="\\"),
333                 GrayIdler(v,one="X",zero="O"),
334                 GrayIdler(v,one="*",zero="-",reorder=1),
335                 GrayIdler(v,one="/",zero="\\",reorder=1),
336                 GrayIdler(v,one="X",zero="O",reorder=1),
337                 #
338                 ClockIdler(v),
339                 ClockIdler(v),
340                 ClockIdler(v),
341                 #
342                 StringIdler(v), # Hello Cruel World
343                 StringIdler(v, text="Kill 'em all", repeat=False),
344                 StringIdler(v, text=CREDITS),
345                 StringIdler(v, text=str(math.pi) + "            "),
346                 StringIdler(v, text=str(math.e) + "            "),
347                 StringIdler(v, text="    I want some pizza - please call Broadway Pizza on +61 8 9389 8500 - and order as Quinn - I am getting really hungry", repeat=False),
348                 # "Hello World" in brainfuck
349                 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
350                 #
351                 TrainIdler(v),
352                 #
353                 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
354                 #
355                 PipeIdler(v, "/usr/bin/getent", "passwd"),
356                 FortuneIdler(v,affinity=20),
357                 ]
358         disabled = [
359                 ]
360
361 """
362 Go back to the default idler.
363 """
364 def reset_idler(v, vstatus, t = None):
365         global idlers, idler
366         idler = GreetingIdler(v, t)
367         vstatus.time_of_next_idlestep = time()+idler.next()
368         vstatus.time_of_next_idler = None
369         vstatus.time_to_autologout = None
370         vstatus.change_state(STATE_IDLE, 1)
371
372 """
373 Change to a random idler.
374 """
375 def choose_idler():
376         global idlers, idler
377
378         # Implementation of the King Of the Hill algorithm from;
379         # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
380
381         #def weighted_choice_king(weights):
382         #       total = 0
383         #       winner = 0
384         #       for i, w in enumerate(weights):
385         #               total += w
386         #               if random.random() * total < w:
387         #                       winner = i
388         #       return winner
389         #       
390
391         total = 0
392         winner = None
393         
394         for choice in idlers:
395                 weight = choice.affinity()
396                 total += weight
397                 if random() * total < weight:
398                         winner = choice
399
400         idler = winner
401
402         if idler:
403                 idler.reset()
404 """
405 Run every step while the machine is idling.
406 """
407 def idle_step(vstatus):
408         global idler
409         if idler.finished():
410                 choose_idler()
411                 vstatus.time_of_next_idler = time() + 30
412         nextidle = idler.next()
413         if nextidle is None:
414                 nextidle = IDLE_SPEED
415         vstatus.time_of_next_idlestep = time()+nextidle
416
417 """
418 These next two events trigger no response in the code.
419 """
420 def handle_tick_event(event, params, v, vstatus):
421         # don't care right now.
422         pass
423
424 def handle_switch_event(event, params, v, vstatus):
425         # don't care right now.
426         pass
427
428 """
429 Don't do anything for this event.
430 """
431 def do_nothing(state, event, params, v, vstatus):
432         print "doing nothing (s,e,p)", state, " ", event, " ", params
433         pass
434
435 """
436 These next few entrie tell us to do nothing while we are idling
437 """
438 def handle_getting_uid_idle(state, event, params, v, vstatus):
439         # don't care right now.
440         pass
441
442 def handle_getting_pin_idle(state, event, params, v, vstatus):
443         # don't care right now.
444         pass
445
446 """
447 While logged in and waiting for user input, slowly get closer to logging out.
448 """
449 def handle_get_selection_idle(state, event, params, v, vstatus):
450         global _last_card_id
451         # don't care right now.
452         ###
453         ### State logging out ..
454         if vstatus.time_to_autologout != None:
455                 time_left = vstatus.time_to_autologout - time()
456                 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
457                         vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
458                         vstatus.last_timeout_refresh = int(time_left)
459                         vstatus.cur_selection = ''
460
461         if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
462                 vstatus.time_to_autologout = None
463                 vstatus.cur_user = ''
464                 vstatus.cur_pin = ''
465                 vstatus.cur_selection = ''
466                 _last_card_id = -1
467                 reset_idler(v, vstatus)
468
469         ### State fully logged out ... reset variables
470         if vstatus.time_to_autologout and not vstatus.mk.done(): 
471                 vstatus.time_to_autologout = None
472         if vstatus.cur_user == '' and vstatus.time_to_autologout: 
473                 vstatus.time_to_autologout = None
474         
475         ### State logged in
476         if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
477                 # start autologout
478                 vstatus.time_to_autologout = time() + 15
479                 vstatus.last_timeout_refresh = None
480
481         ## FIXME - this may need to be elsewhere.....
482         # need to check
483         vstatus.mk.update_display()
484
485 """
486 Triggered on user input while logged in.
487 """
488 def handle_get_selection_key(state, event, params, v, vstatus):
489         global _last_card_id
490         key = params
491         if len(vstatus.cur_selection) == 0:
492                 if key == 11:
493                         vstatus.cur_pin = ''
494                         vstatus.cur_user = ''
495                         vstatus.cur_selection = ''
496                         _last_card_id = -1
497                         vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
498                         reset_idler(v, vstatus, 2)
499                         return
500                 vstatus.cur_selection += chr(key + ord('0'))
501                 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
502                 vstatus.time_to_autologout = None
503         elif len(vstatus.cur_selection) == 1:
504                 if key == 11:
505                         vstatus.cur_selection = ''
506                         vstatus.time_to_autologout = None
507                         scroll_options(vstatus.username, vstatus.mk)
508                         return
509                 else:
510                         vstatus.cur_selection += chr(key + ord('0'))
511                         if vstatus.cur_user:
512                                 make_selection(v,vstatus)
513                                 vstatus.cur_selection = ''
514                                 vstatus.time_to_autologout = time() + 8
515                                 vstatus.last_timeout_refresh = None
516                         else:
517                                 # Price check mode.
518                                 price_check(v,vstatus)
519                                 vstatus.cur_selection = ''
520                                 vstatus.time_to_autologout = None
521                                 vstatus.last_timeout_refresh = None
522
523 """
524 Triggered when the user has entered the id of something they would like to purchase.
525 """
526 def make_selection(v, vstatus):
527         # should use sudo here
528         if vstatus.cur_selection == '55':
529                 vstatus.mk.set_message('OPENSESAME')
530                 logging.info('dispensing a door for %s'%vstatus.username)
531                 if geteuid() == 0:
532                         ret = os.system('dispense -u "%s" door'%vstatus.username)
533                 else:
534                         ret = os.system('dispense door')
535                 if ret == 0:
536                         logging.info('door opened')
537                         vstatus.mk.set_message(center('DOOR OPEN'))
538                 else:
539                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
540                         vstatus.mk.set_message(center('BAD DOOR'))
541                 sleep(1)
542         elif vstatus.cur_selection == '81':
543                 cookie(v)
544         elif vstatus.cur_selection == '99':
545                 scroll_options(vstatus.username, vstatus.mk)
546                 vstatus.cur_selection = ''
547                 return
548         elif vstatus.cur_selection[1] == '8':
549                 v.display('GOT DRINK?')
550                 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
551                         v.display('SEEMS NOT')
552                 else:
553                         v.display('GOT DRINK!')
554         else:
555                 # first see if it's a named slot
556                 try:
557                         price, shortname, name = get_snack( vstatus.cur_selection )
558                 except:
559                         price, shortname, name = get_snack( '--' )
560                 dollarprice = "$%.2f" % ( price / 100.0 )
561                 v.display(vstatus.cur_selection+' - %s'%dollarprice)
562                 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
563                 if (exitcode == 0):
564                         # magic dispense syslog service
565                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
566                         (worked, code, string) = v.vend(vstatus.cur_selection)
567                         if worked:
568                                 v.display('THANK YOU')
569                         else:
570                                 print "Vend Failed:", code, string
571                                 v.display('VEND FAIL')
572                 elif (exitcode == 5):   # RV_BALANCE
573                         v.display('NO MONEY?')
574                 elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
575                         v.display('EMPTY SLOT')
576                 elif (exitcode == 1):   # RV_BADITEM (Dead slot)
577                         v.display('EMPTY SLOT')
578                 else:
579                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
580                         v.display('UNK ERROR')
581         sleep(1)
582
583 """
584 Find the price of an item.
585 """
586 def price_check(v, vstatus):
587         if vstatus.cur_selection[1] == '8':
588                 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
589                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
590                 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
591         else:
592                 # first see if it's a named slot
593                 try:
594                         price, shortname, name = get_snack( vstatus.cur_selection )
595                 except:
596                         price, shortname, name = get_snack( '--' )
597                 dollarprice = "$%.2f" % ( price / 100.0 )
598         v.display(vstatus.cur_selection+' - %s'%dollarprice)
599
600 """
601 Triggered when the user presses a button while entering their pin.
602 """
603 def handle_getting_pin_key(state, event, params, v, vstatus):
604         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
605         key = params
606         if len(vstatus.cur_pin) < PIN_LENGTH:
607                 if key == 11:
608                         if vstatus.cur_pin == '':
609                                 vstatus.cur_user = ''
610                                 reset_idler(v, vstatus)
611
612                                 return
613                         vstatus.cur_pin = ''
614                         vstatus.mk.set_message('PIN: ')
615                         return
616                 vstatus.cur_pin += chr(key + ord('0'))
617                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
618                 if len(vstatus.cur_pin) == PIN_LENGTH:
619                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
620                         if vstatus.username:
621                                 v.beep(0, False)
622                                 vstatus.cur_selection = ''
623                                 vstatus.change_state(STATE_GET_SELECTION)
624                                 scroll_options(vstatus.username, vstatus.mk, True)
625                                 return
626                         else:
627                                 v.beep(40, False)
628                                 vstatus.mk.set_messages(
629                                         [(center('BAD PIN'), False, 1.0),
630                                          (center('SORRY'), False, 0.5)])
631                                 vstatus.cur_user = ''
632                                 vstatus.cur_pin = ''
633                         
634                                 reset_idler(v, vstatus, 2)
635
636                                 return
637
638 """
639 Triggered when the user presses a button while entering their user id.
640 """
641 def handle_getting_uid_key(state, event, params, v, vstatus):
642         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
643         key = params
644
645         # complicated key handling here:
646
647         if len(vstatus.cur_user) == 0 and key == 9:
648                 vstatus.cur_selection = ''
649                 vstatus.time_to_autologout = None
650                 vstatus.mk.set_message('PRICECHECK')
651                 sleep(0.5)
652                 scroll_options('', vstatus.mk)
653                 vstatus.change_state(STATE_GET_SELECTION)
654                 return
655
656         if len(vstatus.cur_user) <8:
657                 if key == 11:
658                         vstatus.cur_user = ''
659
660                         reset_idler(v, vstatus)
661                         return
662                 vstatus.cur_user += chr(key + ord('0'))
663                 #logging.info('dob: '+vstatus.cur_user)
664                 if len(vstatus.cur_user) > 5:
665                         vstatus.mk.set_message('>'+vstatus.cur_user)
666                 else:
667                         vstatus.mk.set_message('UID: '+vstatus.cur_user)
668         
669         if len(vstatus.cur_user) == 5:
670                 uid = int(vstatus.cur_user)
671
672                 if uid == 0:
673                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
674                         pfalken="""
675 CARRIER DETECTED
676
677 CONNECT 128000
678
679 Welcome to Picklevision Sytems, Sunnyvale, CA
680
681 Greetings Professor Falken.
682
683
684
685
686 Shall we play a game?
687
688
689 Please choose from the following menu:
690
691 1. Tic-Tac-Toe
692 2. Chess
693 3. Checkers
694 4. Backgammon
695 5. Poker
696 6. Toxic and Biochemical Warfare
697 7. Global Thermonuclear War
698
699 7 [ENTER]
700
701 Wouldn't you prefer a nice game of chess?
702
703 """.replace('\n','    ')
704                         vstatus.mk.set_messages([(pfalken, False, 10)])
705                         vstatus.cur_user = ''
706                         vstatus.cur_pin = ''
707                         
708                         reset_idler(v, vstatus, 10)
709
710                         return
711
712                 if not has_good_pin(uid):
713                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
714                         vstatus.mk.set_messages(
715                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
716                         vstatus.cur_user = ''
717                         vstatus.cur_pin = ''
718                         
719                         reset_idler(v, vstatus, 3)
720
721                         return
722                 
723                 if acct_is_disabled():
724                         logging.info('user '+vstatus.cur_user+' is disabled')
725                         vstatus.mk.set_messages(
726                                 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
727                         vstatus.cur_user = ''
728                         vstatus.cur_pin = ''
729                         
730                         reset_idler(v, vstatus, 3)
731                         return
732
733
734                 vstatus.cur_pin = ''
735                 vstatus.mk.set_message('PIN: ')
736                 logging.info('need pin for user %s'%vstatus.cur_user)
737                 vstatus.change_state(STATE_GETTING_PIN)
738                 return
739
740 """
741 Triggered when a key is pressed and the machine is idling.
742 """
743 def handle_idle_key(state, event, params, v, vstatus):
744         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
745
746         key = params
747
748         if key == 11:
749                 vstatus.cur_user = ''
750                 reset_idler(v, vstatus)
751                 return
752         
753         vstatus.change_state(STATE_GETTING_UID)
754         run_handler(event, key, v, vstatus)
755
756 """
757 What to do when there is nothing to do.
758 """
759 def handle_idle_tick(state, event, params, v, vstatus):
760         ### State idling
761         if vstatus.mk.done():
762                 idle_step(vstatus)
763
764         if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
765                 vstatus.time_of_next_idler = time() + 30
766                 choose_idler()
767         
768         ###
769
770         vstatus.mk.update_display()
771
772         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
773         run_handler(event, params, v, vstatus)
774         sleep(0.05)
775
776 """
777 Manages the beeps for the grandfather clock
778 """
779 def beep_on(when, before=0):
780         start = int(when - before)
781         end = int(when)
782         now = int(time())
783
784         if now >= start and now <= end:
785                 return 1
786         return 0
787
788 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
789         ### check for interesting times
790         now = localtime()
791
792         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
793         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
794         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
795         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
796
797         hourfromnow = localtime(time() + 3600)
798         
799         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
800         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
801                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
802
803         ## check for X seconds to the hour
804         ## if case, update counter to 2
805         if beep_on(onthehour,15) \
806                 or beep_on(halfhour,0) \
807                 or beep_on(quarterhour,0) \
808                 or beep_on(threequarterhour,0) \
809                 or beep_on(fivetothehour,0):
810                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
811                 run_handler(event, params, v, vstatus)
812         else:
813                 vstatus.change_state(STATE_IDLE)
814
815 def handle_grandfather_tick(state, event, params, v, vstatus):
816         go_idle = 1
817
818         msg = []
819         ### we live in interesting times
820         now = localtime()
821
822         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
823         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
824         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
825         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
826
827         hourfromnow = localtime(time() + 3600)
828         
829 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
830         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
831                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
832
833
834         #print "when it fashionable to wear a onion on your hip"
835
836         if beep_on(onthehour,15):
837                 go_idle = 0
838                 next_hour=((hourfromnow[3] + 11) % 12) + 1
839                 if onthehour - time() < next_hour and onthehour - time() > 0:
840                         v.beep(0, False)
841
842                         t = int(time())
843                         if (t % 2) == 0:
844                                 msg.append(("DING!", False, None))
845                         else:
846                                 msg.append(("     DING!", False, None))
847                 elif int(onthehour - time()) == 0:
848                         v.beep(255, False)
849                         msg.append(("   BONG!", False, None))
850                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
851         elif beep_on(halfhour,0):
852                 go_idle = 0
853                 v.beep(0, False)
854                 msg.append((" HALFHOUR ", False, 50))
855         elif beep_on(quarterhour,0):
856                 go_idle = 0
857                 v.beep(0, False)
858                 msg.append((" QTR HOUR ", False, 50))
859         elif beep_on(threequarterhour,0):
860                 go_idle = 0
861                 v.beep(0, False)
862                 msg.append((" 3 QTR HR ", False, 50))
863         elif beep_on(fivetothehour,0):
864                 go_idle = 0
865                 v.beep(0, False)
866                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
867         else:
868                 go_idle = 1
869         
870         ## check for X seconds to the hour
871
872         if len(msg):
873                 vstatus.mk.set_messages(msg)
874                 sleep(1)
875
876         vstatus.mk.update_display()
877         ## if no longer case, return to idle
878
879         ## change idler to be clock
880         if go_idle and vstatus.mk.done():
881                 vstatus.change_state(STATE_IDLE,1)
882
883 """
884 What to do when the door is open.
885 """
886 def handle_door_idle(state, event, params, v, vstatus):
887         def twiddle(clock,v,wise = 2):
888                 if (clock % 4 == 0):
889                         v.display("-FEED  ME-")
890                 elif (clock % 4 == 1+wise):
891                         v.display("\\FEED  ME/")
892                 elif (clock % 4 == 2):
893                         v.display("-FEED  ME-")
894                 elif (clock % 4 == 3-wise):
895                         v.display("/FEED  ME\\")
896
897         # don't care right now.
898         now = int(time())
899
900         if ((now % 60 % 2) == 0):
901                 twiddle(now, v)
902         else:
903                 twiddle(now, v, wise=0)
904
905 """
906 What to do when the door is opened or closed.
907 """
908 def handle_door_event(state, event, params, v, vstatus):
909         if params == 0:  #door open
910                 vstatus.change_state(STATE_DOOR_OPENING)
911                 logging.warning("Entering open door mode")
912                 v.display("-FEED  ME-")
913                 #door_open_mode(v);
914                 vstatus.cur_user = ''
915                 vstatus.cur_pin = ''
916         elif params == 1:  #door closed
917                 vstatus.change_state(STATE_DOOR_CLOSING)
918                 reset_idler(v, vstatus, 3)
919
920                 logging.warning('Leaving open door mode')
921                 v.display("-YUM YUM!-")
922
923 """
924 Triggered when a user swipes their caed, and the machine is logged out.
925 """
926 def handle_mifare_event(state, event, params, v, vstatus):
927         global _last_card_id
928         card_id = params
929         # Translate card_id into uid.
930         if card_id == None or card_id == _last_card_id:
931                 return
932
933         _last_card_id = card_id
934         
935         try:
936                 vstatus.cur_user = get_uid(card_id)
937                 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
938                 vstatus.username = get_uname(vstatus.cur_user)
939                 if acct_is_disabled(vstatus.username):
940                         vstatus.username = '-disabled-'
941         except ValueError:
942                 vstatus.username = None
943         if vstatus.username == '-disabled-':
944                 v.beep(40, False)
945                 vstatus.mk.set_messages(
946                         [(center('ACCT DISABLED'), False, 1.0),
947                          (center('SORRY'), False, 0.5)])
948                 vstatus.cur_user = ''
949                 vstatus.cur_pin = ''
950                 vstatus.username = None
951         
952                 reset_idler(v, vstatus, 2)
953                 return
954         elif vstatus.username:
955                 v.beep(0, False)
956                 vstatus.cur_selection = ''
957                 vstatus.change_state(STATE_GET_SELECTION)
958                 scroll_options(vstatus.username, vstatus.mk, True)
959                 return
960         else:
961                 v.beep(40, False)
962                 vstatus.mk.set_messages(
963                         [(center('BAD CARD'), False, 1.0),
964                          (center('SORRY'), False, 0.5)])
965                 vstatus.cur_user = ''
966                 vstatus.cur_pin = ''
967                 _last_card_id = -1
968         
969                 reset_idler(v, vstatus, 2)
970                 return
971
972 """
973 Triggered when a user swipes their card and the machine is logged in.
974 """
975 def handle_mifare_add_user_event(state, event, params, v, vstatus):
976         global _last_card_id
977         card_id = params
978
979         # Translate card_id into uid.
980         if card_id == None or card_id == _last_card_id:
981                 return
982
983         _last_card_id = card_id
984         
985         try:
986                 if get_uid(card_id) != None:
987                         vstatus.mk.set_messages(
988                                 [(center('ALREADY'), False, 0.5),
989                                  (center('ENROLLED'), False, 0.5)])
990
991                         # scroll_options(vstatus.username, vstatus.mk)
992                         return
993         except ValueError:
994                 pass
995
996         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
997         set_card_id(vstatus.cur_user, card_id)
998         vstatus.mk.set_messages(
999                 [(center('CARD'), False, 0.5),
1000                  (center('ENROLLED'), False, 0.5)])
1001
1002         # scroll_options(vstatus.username, vstatus.mk)
1003
1004 def return_to_idle(state,event,params,v,vstatus):
1005         reset_idler(v, vstatus)
1006
1007 """
1008 Maps what to do when the state changes.
1009 """
1010 def create_state_table(vstatus):
1011         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
1012         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
1013         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
1014         vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
1015
1016         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
1017         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
1018         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
1019         vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
1020
1021         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
1022         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
1023         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
1024         vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
1025
1026         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
1027         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
1028         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
1029         vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
1030
1031         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
1032         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
1033         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
1034         vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
1035
1036         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
1037         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
1038         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
1039         vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
1040
1041         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
1042         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
1043         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
1044         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
1045         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
1046         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
1047         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
1048
1049 """
1050 Get what to do on a state change.
1051 """
1052 def get_state_table_handler(vstatus, state, event, counter):
1053         return vstatus.state_table[(state,event,counter)]
1054
1055 def time_to_next_update(vstatus):
1056         idle_update = vstatus.time_of_next_idlestep - time()
1057         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
1058                 mk_update = vstatus.mk.next_update - time()
1059                 if mk_update < idle_update:
1060                         idle_update = mk_update
1061         return idle_update
1062
1063 def run_forever(rfh, wfh, options, cf):
1064         v = VendingMachine(rfh, wfh, USE_MIFARE)
1065         vstatus = VendState(v)
1066         create_state_table(vstatus)
1067
1068         logging.debug('PING is ' + str(v.ping()))
1069
1070         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
1071
1072         setup_idlers(v)
1073         reset_idler(v, vstatus)
1074
1075         while True:
1076                 if USE_DB:
1077                         try:
1078                                 db.handle_events()
1079                         except DispenseDatabaseException, e:
1080                                 logging.error('Database error: '+str(e))
1081
1082                 timeout = time_to_next_update(vstatus)
1083                 e = v.next_event(timeout)
1084                 (event, params) = e
1085
1086                 run_handler(event, params, v, vstatus)
1087
1088 def run_handler(event, params, v, vstatus):
1089         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1090         if handler:
1091                 handler(vstatus.state, event, params, v, vstatus)
1092
1093 """
1094 Connect to the machine.
1095 """
1096 def connect_to_vend(options, cf):
1097
1098         if options.use_lat:
1099                 logging.info('Connecting to vending machine using LAT')
1100                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1101                 rfh, wfh = latclient.get_fh()
1102         elif options.use_serial:
1103                 # Open vending machine via serial.
1104                 logging.info('Connecting to vending machine using serial')
1105                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1106                 rfh,wfh = serialclient.get_fh()
1107         else:
1108                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1109                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1110                 import socket
1111                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1112                 sock.connect((options.host, options.port))
1113                 rfh = sock.makefile('r')
1114                 wfh = sock.makefile('w')
1115                 global USE_MIFARE
1116                 USE_MIFARE = 0
1117                 
1118         return rfh, wfh
1119
1120 """
1121 Parse arguments from the command line
1122 """
1123 def parse_args():
1124         from optparse import OptionParser
1125
1126         op = OptionParser(usage="%prog [OPTION]...")
1127         op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
1128         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1129         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1130         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1131         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1132         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1133         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1134         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1135         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1136         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1137         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1138         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1139         options, args = op.parse_args()
1140
1141         if len(args) != 0:
1142                 op.error('extra command line arguments: ' + ' '.join(args))
1143
1144         return options
1145
1146 def create_pid_file(name):
1147         try:
1148                 pid_file = file(name, 'w')
1149                 pid_file.write('%d\n'%os.getpid())
1150                 pid_file.close()
1151         except IOError, e:
1152                 logging.warning('unable to write to pid file '+name+': '+str(e))
1153
1154 def set_stuff_up():
1155         def do_nothing(signum, stack):
1156                 signal.signal(signum, do_nothing)
1157         def stop_server(signum, stack): raise KeyboardInterrupt
1158         signal.signal(signal.SIGHUP, do_nothing)
1159         signal.signal(signal.SIGTERM, stop_server)
1160         signal.signal(signal.SIGINT, stop_server)
1161
1162         options = parse_args()
1163         config_opts = VendConfigFile(options.config_file, config_options)
1164         if options.daemon: become_daemon()
1165         set_up_logging(options)
1166         if options.pid_file != '': create_pid_file(options.pid_file)
1167
1168         return options, config_opts
1169
1170 def clean_up_nicely(options, config_opts):
1171         if options.pid_file != '':
1172                 try:
1173                         os.unlink(options.pid_file)
1174                         logging.debug('Removed pid file '+options.pid_file)
1175                 except OSError: pass  # if we can't delete it, meh
1176
1177 def set_up_logging(options):
1178         logger = logging.getLogger()
1179         
1180         if not options.daemon:
1181                 stderr_logger = logging.StreamHandler(sys.stderr)
1182                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1183                 logger.addHandler(stderr_logger)
1184         
1185         if options.log_file != '':
1186                 try:
1187                         file_logger = logging.FileHandler(options.log_file)
1188                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1189                         logger.addHandler(file_logger)
1190                 except IOError, e:
1191                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1192
1193         if options.syslog != None:
1194                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1195                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1196                 logger.addHandler(sys_logger)
1197
1198         if options.quiet:
1199                 logger.setLevel(logging.WARNING)
1200         elif options.verbose:
1201                 logger.setLevel(logging.DEBUG)
1202         else:
1203                 logger.setLevel(logging.INFO)
1204
1205 def become_daemon():
1206         dev_null = file('/dev/null')
1207         fd = dev_null.fileno()
1208         os.dup2(fd, 0)
1209         os.dup2(fd, 1)
1210         os.dup2(fd, 2)
1211         try:
1212                 if os.fork() != 0:
1213                         sys.exit(0)
1214                 os.setsid()
1215         except OSError, e:
1216                 raise SystemExit('failed to fork: '+str(e))
1217
1218 def do_vend_server(options, config_opts):
1219         while True:
1220                 try:
1221                         rfh, wfh = connect_to_vend(options, config_opts)
1222                 except (SerialClientException, socket.error), e:
1223                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1224                         del exc_traceback
1225                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1226                         logging.info("Trying again in 5 seconds.")
1227                         sleep(5)
1228                         continue
1229                 
1230 #               run_forever(rfh, wfh, options, config_opts)
1231                 
1232                 try:
1233                         run_forever(rfh, wfh, options, config_opts)
1234                 except VendingException:
1235                         logging.error("Connection died, trying again...")
1236                         logging.info("Trying again in 5 seconds.")
1237                         sleep(5)
1238
1239
1240 def main(argv=None):
1241         options, config_opts = set_stuff_up()
1242         while True:
1243                 try:
1244                         logging.warning('Starting Vend Server')
1245                         do_vend_server(options, config_opts)
1246                         logging.error('Vend Server finished unexpectedly, restarting')
1247                 except KeyboardInterrupt:
1248                         logging.info("Killed by signal, cleaning up")
1249                         clean_up_nicely(options, config_opts)
1250                         logging.warning("Vend Server stopped")
1251                         break
1252                 except SystemExit:
1253                         break
1254                 except:
1255                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1256                         tb = format_tb(exc_traceback, 20)
1257                         del exc_traceback
1258                         
1259                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1260                         logging.critical("Message: " + str(exc_value))
1261                         logging.critical("Traceback:")
1262                         for event in tb:
1263                                 for line in event.split('\n'):
1264                                         logging.critical('    '+line)
1265                         logging.critical("This message should be considered a bug in the Vend Server.")
1266                         logging.critical("Please report this to someone who can fix it.")
1267                         sleep(10)
1268                         logging.warning("Trying again anyway (might not help, but hey...)")
1269
1270 if __name__ == '__main__':
1271         sys.exit(main())

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