4e0b16c98ae119262bf03d4aacfcdf8a494ddbaa
[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 traceback import print_tb
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                                                 ret = os.system('su - "%s" -c "dispense door"'%username)
344                                                 if ret == 0:
345                                                         mk.set_message(center('DOOR OPEN'))
346                                                 else:
347                                                         mk.set_message(center('BAD DOOR'))
348                                                 sleep(1)
349                                         elif cur_selection == '91':
350                                                 cookie(v)
351                                         elif cur_selection == '99':
352                                                 scroll_options(username, mk)
353                                                 cur_selection = ''
354                                                 continue
355                                         elif cur_selection[1] == '8':
356                                                 v.display('GOT COKE?')
357                                                 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
358                                         else:
359                                                 v.display('HERES A '+cur_selection)
360                                                 v.vend(cur_selection)
361                                         sleep(0.5)
362                                         v.display('THANK YOU')
363                                         sleep(0.5)
364                                         cur_selection = ''
365                                         time_to_autologout = time() + 8
366
367 def connect_to_vend(options, cf):
368         # Open vending machine via LAT?
369         if options.use_lat:
370                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
371                 rfh, wfh = latclient.get_fh()
372         else:
373                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
374                 import socket
375                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
376                 sock.connect((options.host, options.port))
377                 rfh = sock.makefile('r')
378                 wfh = sock.makefile('w')
379                 
380         return rfh, wfh
381
382 def parse_args():
383         from optparse import OptionParser
384
385         op = OptionParser(usage="%prog [OPTION]...")
386         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')
387         op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
388         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
389         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
390         options, args = op.parse_args()
391
392         if len(args) != 0:
393                 op.error('extra command line arguments: ' + ' '.join(args))
394
395         return options
396
397 config_options = {
398         'DBServer': ('Database', 'Server'),
399         'DBName': ('Database', 'Name'),
400         'DBUser': ('VendingMachine', 'DBUser'),
401         'DBPassword': ('VendingMachine', 'DBPassword'),
402         
403         'ServiceName': ('VendingMachine', 'ServiceName'),
404         'ServicePassword': ('VendingMachine', 'Password'),
405         
406         'ServerName': ('DecServer', 'Name'),
407         'ConnectPassword': ('DecServer', 'ConnectPassword'),
408         'PrivPassword': ('DecServer', 'PrivPassword'),
409         }
410
411 class VendConfigFile:
412         def __init__(self, config_file, options):
413                 try:
414                         cp = ConfigParser.ConfigParser()
415                         cp.read(config_file)
416
417                         for option in options:
418                                 section, name = options[option]
419                                 value = cp.get(section, name)
420                                 self.__dict__[option] = value
421                 
422                 except ConfigParser.Error, e:
423                         print "Error reading config file "+config_file+": " + str(e)
424                         sys.exit(1)
425
426 def do_vend_server():
427         options = parse_args()
428         config_opts = VendConfigFile(options.config_file, config_options)
429
430         while True:
431                 try:
432                         rfh, wfh = connect_to_vend(options, config_opts)
433                 except (LATClientException, socket.error), e:
434                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
435                         del exc_traceback
436                         print
437                         print "Connection error: "+str(exc_type)+" "+str(e)
438                         print "Trying again in 5 seconds."
439                         sleep(5)
440                         continue
441                 try:
442                         run_forever(rfh, wfh, options, config_opts)
443                 except VendingException:
444                         print
445                         print "Connection died, trying again..."
446
447
448 if __name__ == '__main__':
449         while True:
450                 try:
451                         do_vend_server()
452                 except KeyboardInterrupt:
453                         print "Killed by SIGINT."
454                         break
455                 except:
456                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
457                         print
458                         print "Uh-oh, unhandled " + str(exc_type) + " exception"
459                         print "Message: ", str(exc_value)
460                         print
461                         print_tb(exc_traceback)
462                         del exc_traceback
463                         print
464                         print "This message should be considered a bug in the Vend Server."
465                         print "Please report this to someone who can fix it."
466                         print
467                         print "Trying again anyway (might not help, but hey...)"
468

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