8d1f4328014d3d6cf3154ce914a6be0ab29c6d14
[zanchey/dispense2.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
8 from traceback import print_tb
9 if USE_DB: import pg
10 from time import time, sleep
11 from popen2 import popen2
12 from LATClient import LATClient, LATClientException
13 from VendingMachine import VendingMachine, VendingException
14 from HorizScroll import HorizScroll
15 from random import random, seed
16 from Idler import TrainIdler,GrayIdler
17 import socket
18 from posix import geteuid
19
20 GREETING = 'UCC SNACKS'
21 PIN_LENGTH = 4
22
23 DOOR = 1
24 SWITCH = 2
25 KEY = 3
26
27 class DispenseDatabase:
28         def __init__(self, vending_machine, host, name, user, password):
29                 self.vending_machine = vending_machine
30                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
31                 self.db.query('LISTEN vend_requests')
32
33         def process_requests(self):
34                 print 'processing'
35                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
36                 try:
37                         outstanding = self.db.query(query).getresult()
38                 except (pg.error,), db_err:
39                         sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
40                         return
41                 for (id, slot) in outstanding:
42                         (worked, code, string) = self.vending_machine.vend(slot)
43                         print (worked, code, string)
44                         if worked:
45                                 query = 'SELECT vend_success(%s)'%id
46                                 self.db.query(query).getresult()
47                         else:
48                                 query = 'SELECT vend_failed(%s)'%id
49                                 self.db.query(query).getresult()
50
51         def handle_events(self):
52                 notifier = self.db.getnotify()
53                 while notifier is not None:
54                         self.process_requests()
55                         notify = self.db.getnotify()
56
57 def scroll_options(username, mk, welcome = False):
58         if welcome:
59                 msg = [(center('WELCOME'), False, 0.8),
60                            (center(username), False, 0.8)]
61         else:
62                 msg = []
63         choices = ' '*10+'CHOICES: '
64         try:
65                 coke_machine = file('/home/other/coke/coke_contents')
66                 cokes = coke_machine.readlines()
67                 coke_machine.close()
68         except:
69                 cokes = []
70                 pass
71         for c in cokes:
72                 c = c.strip()
73                 (slot_num, price, slot_name) = c.split(' ', 2)
74                 if slot_name == 'dead': continue
75                 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
76         choices += '55-DOOR '
77         choices += 'OR A SNACK. '
78         choices += '99 TO READ AGAIN. '
79         choices += 'CHOICE?   '
80         msg.append((choices, False, None))
81         mk.set_messages(msg)
82
83 def get_pin(uid):
84         try:
85                 info = pwd.getpwuid(uid)
86         except KeyError:
87                 return None
88         if info.pw_dir == None: return False
89         pinfile = os.path.join(info.pw_dir, '.pin')
90         try:
91                 s = os.stat(pinfile)
92         except OSError:
93                 return None
94         if s.st_mode & 077:
95                 return None
96         try:
97                 f = file(pinfile)
98         except IOError:
99                 return None
100         pinstr = f.readline()
101         f.close()
102         if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
103                 return None
104         return int(pinstr)
105
106 def has_good_pin(uid):
107         return get_pin(uid) != None
108
109 def verify_user_pin(uid, pin):
110         if get_pin(uid) == pin:
111                 info = pwd.getpwuid(uid)
112                 return info.pw_name
113         else:
114                 return None
115
116 def door_open_mode(v):
117         print "Entering open door mode"
118         v.display("-FEED  ME-")
119         while True:
120                 e = v.next_event()
121                 if e == None: break
122                 (event, params) = e
123                 if event == DOOR:
124                         if params == 1: # door closed
125                                 v.display("-YUM YUM!-")
126                                 sleep(1)
127                                 return
128
129 def cookie(v):
130         seed(time())
131         messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
132         choice = int(random()*len(messages))
133         msg = messages[choice]
134         left = range(len(msg))
135         for i in range(len(msg)):
136                 if msg[i] == ' ': left.remove(i)
137         reveal = 1
138         while left:
139                 s = ''
140                 for i in range(0, len(msg)):
141                         if i in left:
142                                 if reveal == 0:
143                                         left.remove(i)
144                                         s += msg[i]
145                                 else:
146                                         s += chr(int(random()*26)+ord('A'))
147                                 reveal += 1
148                                 reveal %= 17
149                         else:
150                                 s += msg[i]
151                 v.display(s)
152
153 def center(str):
154         LEN = 10
155         return ' '*((LEN-len(str))/2)+str
156
157 class MessageKeeper:
158         def __init__(self, vendie):
159                 # Each element of scrolling_message should be a 3-tuple of
160                 # ('message', True/False if it is to be repeated, time to display)
161                 self.scrolling_message = []
162                 self.v = vendie
163                 self.next_update = None
164
165         def set_message(self, string):
166                 self.scrolling_message = [(string, False, None)]
167                 self.update_display(True)
168
169         def set_messages(self, strings):
170                 self.scrolling_message = strings
171                 self.update_display(True)
172
173         def update_display(self, forced = False):
174                 if not forced and self.next_update != None and time() < self.next_update:
175                         return
176                 if len(self.scrolling_message) > 0:
177                         if len(self.scrolling_message[0][0]) > 10:
178                                 (m, r, t) = self.scrolling_message[0]
179                                 a = []
180                                 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
181                                 if t == None:
182                                         t = 0.1
183                                 else:
184                                         t = t / len(exp)
185                                 for x in exp:
186                                         a.append((x, r, t))
187                                 del self.scrolling_message[0]
188                                 self.scrolling_message = a + self.scrolling_message
189                         newmsg = self.scrolling_message[0]
190                         if newmsg[2] != None:
191                                 self.next_update = time() + newmsg[2]
192                         else:
193                                 self.next_update = None
194                         self.v.display(self.scrolling_message[0][0])
195                         if self.scrolling_message[0][1]:
196                                 self.scrolling_message.append(self.scrolling_message[0])
197                         del self.scrolling_message[0]
198
199         def done(self):
200                 return len(self.scrolling_message) == 0
201
202 def run_forever(rfh, wfh, options, cf):
203         v = VendingMachine(rfh, wfh)
204         print 'PING is', v.ping()
205
206         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
207         cur_user = ''
208         cur_pin = ''
209         cur_selection = ''
210
211         mk = MessageKeeper(v)
212         mk.set_message(GREETING)
213         time_to_autologout = None
214         #idler = TrainIdler(v)
215         #idler = GrayIdler(v)
216         idler = GrayIdler(v,one="*",zero="-")
217         time_to_idle = None
218         last_timeout_refresh = None
219
220         while True:
221                 if USE_DB: db.handle_events()
222
223                 if time_to_autologout != None:
224                         time_left = time_to_autologout - time()
225                         if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
226                                 mk.set_message('LOGOUT: '+str(int(time_left)))
227                                 last_timeout_refresh = int(time_left)
228                                 cur_selection = ''
229
230                 if time_to_autologout != None and time_to_autologout - time() <= 0:
231                         time_to_autologout = None
232                         cur_user = ''
233                         cur_pin = ''
234                         cur_selection = ''
235                         mk.set_message(GREETING)
236
237                 if time_to_autologout and not mk.done(): time_to_autologout = None
238                 if cur_user == '' and time_to_autologout: time_to_autologout = None
239                 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
240                         # start autologout
241                         time_to_autologout = time() + 15
242
243                 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
244                 if time_to_idle != None and cur_user != '': time_to_idle = None
245                 if time_to_idle is not None and time() > time_to_idle: idler.next()
246
247                 mk.update_display()
248
249                 e = v.next_event(0)
250                 if e == None:
251                         e = v.next_event(0.1)
252                         if e == None:
253                                 continue
254                 time_to_idle = None
255                 (event, params) = e
256                 print e
257                 if event == DOOR:
258                         if params == 0:
259                                 door_open_mode(v);
260                                 cur_user = ''
261                                 cur_pin = ''
262                                 mk.set_message(GREETING)
263                 elif event == SWITCH:
264                         # don't care right now.
265                         pass
266                 elif event == KEY:
267                         key = params
268                         # complicated key handling here:
269                         if len(cur_user) < 5:
270                                 if key == 11:
271                                         cur_user = ''
272                                         mk.set_message(GREETING)
273                                         continue
274                                 cur_user += chr(key + ord('0'))
275                                 mk.set_message('UID: '+cur_user)
276                                 if len(cur_user) == 5:
277                                         uid = int(cur_user)
278                                         if not has_good_pin(uid):
279                                                 #mk.set_messages(
280                                                         #[(center('INVALID'), False, 0.7),
281                                                          #(center('PIN'), False, 0.7),
282                                                          #(center('SETUP'), False, 1.0),
283                                                          #(GREETING, False, None)])
284                                                 mk.set_messages(
285                                                         [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
286                                                          (GREETING, False, None)])
287                                                 cur_user = ''
288                                                 cur_pin = ''
289                                                 continue
290                                         cur_pin = ''
291                                         mk.set_message('PIN: ')
292                                         continue
293                         elif len(cur_pin) < PIN_LENGTH:
294                                 if key == 11:
295                                         if cur_pin == '':
296                                                 cur_user = ''
297                                                 mk.set_message(GREETING)
298                                                 continue
299                                         cur_pin = ''
300                                         mk.set_message('PIN: ')
301                                         continue
302                                 cur_pin += chr(key + ord('0'))
303                                 mk.set_message('PIN: '+'X'*len(cur_pin))
304                                 if len(cur_pin) == PIN_LENGTH:
305                                         username = verify_user_pin(int(cur_user), int(cur_pin))
306                                         if username:
307                                                 v.beep(0, False)
308                                                 cur_selection = ''
309                                                 scroll_options(username, mk, True)
310                                                 continue
311                                         else:
312                                                 v.beep(40, False)
313                                                 mk.set_messages(
314                                                         [(center('BAD PIN'), False, 1.0),
315                                                          (center('SORRY'), False, 0.5),
316                                                          (GREETING, False, None)])
317                                                 cur_user = ''
318                                                 cur_pin = ''
319                                                 continue
320                         elif len(cur_selection) == 0:
321                                 if key == 11:
322                                         cur_pin = ''
323                                         cur_user = ''
324                                         cur_selection = ''
325                                         mk.set_messages(
326                                                 [(center('BYE!'), False, 1.5),
327                                                  (GREETING, False, None)])
328                                         continue
329                                 cur_selection += chr(key + ord('0'))
330                                 mk.set_message('SELECT: '+cur_selection)
331                                 time_to_autologout = None
332                         elif len(cur_selection) == 1:
333                                 if key == 11:
334                                         cur_selection = ''
335                                         time_to_autologout = None
336                                         scroll_options(username, mk)
337                                         continue
338                                 else:
339                                         cur_selection += chr(key + ord('0'))
340                                         #make_selection(cur_selection)
341                                         # XXX this should move somewhere else:
342                                         if cur_selection == '55':
343                                                 mk.set_message('OPENSESAME')
344                                                 if geteuid() == 0:
345                                                         ret = os.system('su - "%s" -c "dispense door"'%username)
346                                                 else:
347                                                         ret = os.system('dispense door')
348                                                 if ret == 0:
349                                                         mk.set_message(center('DOOR OPEN'))
350                                                 else:
351                                                         mk.set_message(center('BAD DOOR'))
352                                                 sleep(1)
353                                         elif cur_selection == '91':
354                                                 cookie(v)
355                                         elif cur_selection == '99':
356                                                 scroll_options(username, mk)
357                                                 cur_selection = ''
358                                                 continue
359                                         elif cur_selection[1] == '8':
360                                                 v.display('GOT COKE?')
361                                                 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
362                                         else:
363                                                 v.display('HERES A '+cur_selection)
364                                                 v.vend(cur_selection)
365                                         sleep(0.5)
366                                         v.display('THANK YOU')
367                                         sleep(0.5)
368                                         cur_selection = ''
369                                         time_to_autologout = time() + 8
370
371 def connect_to_vend(options, cf):
372         # Open vending machine via LAT?
373         if options.use_lat:
374                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
375                 rfh, wfh = latclient.get_fh()
376         else:
377                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
378                 import socket
379                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
380                 sock.connect((options.host, options.port))
381                 rfh = sock.makefile('r')
382                 wfh = sock.makefile('w')
383                 
384         return rfh, wfh
385
386 def parse_args():
387         from optparse import OptionParser
388
389         op = OptionParser(usage="%prog [OPTION]...")
390         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')
391         op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
392         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
393         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
394         options, args = op.parse_args()
395
396         if len(args) != 0:
397                 op.error('extra command line arguments: ' + ' '.join(args))
398
399         return options
400
401 config_options = {
402         'DBServer': ('Database', 'Server'),
403         'DBName': ('Database', 'Name'),
404         'DBUser': ('VendingMachine', 'DBUser'),
405         'DBPassword': ('VendingMachine', 'DBPassword'),
406         
407         'ServiceName': ('VendingMachine', 'ServiceName'),
408         'ServicePassword': ('VendingMachine', 'Password'),
409         
410         'ServerName': ('DecServer', 'Name'),
411         'ConnectPassword': ('DecServer', 'ConnectPassword'),
412         'PrivPassword': ('DecServer', 'PrivPassword'),
413         }
414
415 class VendConfigFile:
416         def __init__(self, config_file, options):
417                 try:
418                         cp = ConfigParser.ConfigParser()
419                         cp.read(config_file)
420
421                         for option in options:
422                                 section, name = options[option]
423                                 value = cp.get(section, name)
424                                 self.__dict__[option] = value
425                 
426                 except ConfigParser.Error, e:
427                         print "Error reading config file "+config_file+": " + str(e)
428                         sys.exit(1)
429
430 def do_vend_server():
431         options = parse_args()
432         config_opts = VendConfigFile(options.config_file, config_options)
433
434         while True:
435                 try:
436                         rfh, wfh = connect_to_vend(options, config_opts)
437                 except (LATClientException, socket.error), e:
438                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
439                         del exc_traceback
440                         print
441                         print "Connection error: "+str(exc_type)+" "+str(e)
442                         print "Trying again in 5 seconds."
443                         sleep(5)
444                         continue
445                 try:
446                         run_forever(rfh, wfh, options, config_opts)
447                 except VendingException:
448                         print
449                         print "Connection died, trying again..."
450
451 if __name__ == '__main__':
452         def do_nothing(signum, stack): pass
453         def stop_server(signum, stack): raise KeyboardInterrupt
454         signal.signal(signal.SIGHUP, do_nothing)
455         signal.signal(signal.SIGTERM, stop_server)
456         signal.signal(signal.SIGINT, stop_server)
457         while True:
458                 try:
459                         do_vend_server()
460                 except KeyboardInterrupt:
461                         print "Killed by signal."
462                         break
463                 except:
464                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
465                         print
466                         print "Uh-oh, unhandled " + str(exc_type) + " exception"
467                         print "Message: ", str(exc_value)
468                         print
469                         print_tb(exc_traceback)
470                         del exc_traceback
471                         print
472                         print "This message should be considered a bug in the Vend Server."
473                         print "Please report this to someone who can fix it."
474                         print
475                         print "Trying again anyway (might not help, but hey...)"
476

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