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

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